AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Frontend A11y

skill-affaan-m-ecc-frontend-a11y · by affaan-m

>

— No reviews yet
0 installs
50 views
0.0% view→install

Install

$ agentstack add skill-affaan-m-ecc-frontend-a11y

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • ✓ Prompt-injection patterns
  • ✓ Secret / credential exfiltration
  • ✓ Dangerous shell & filesystem operations
  • ✓ Untrusted network calls
  • ✓ Known-malicious package signatures

What it can access

  • ✓ Network access No
  • ✓ Filesystem access No
  • ✓ Shell / process execution No
  • ✓ Environment & secrets No
  • ✓ Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-affaan-m-ecc-frontend-a11y)

Reliability & compatibility

✓ Security review passed
0 installs to date
— no reviews yet
● 3mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Frontend A11y? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Frontend Accessibility Patterns

Practical accessibility patterns for React and Next.js. Covers the issues most commonly flagged in code review: missing form labels, incorrect ARIA usage, non-semantic interactive elements, and broken keyboard navigation.

When to Activate

  • Building or reviewing form components (`, , `)
  • Creating interactive elements (modals, dropdowns, tooltips, tabs)
  • Using ` or with onClick`
  • Adding aria-* attributes to any element
  • Implementing keyboard navigation or focus management
  • Receiving accessibility feedback from code review tools (CodeRabbit, ESLint a11y)
  • Building components that must support screen readers

Form Accessibility

Missing htmlFor / id pairing and disconnected error messages are the most common issues flagged in code review.

Label Connection

// BAD: label has no connection to input — screen readers cannot associate them
Email

// GOOD: htmlFor matches input id
Email

Required Fields

// BAD: visual-only asterisk conveys nothing to screen readers
Email *

// GOOD: required enables native browser validation; aria-required signals it to screen readers

  Email *

Error Messages

// BAD: error text exists visually but is not linked to the input

Invalid email address

// GOOD: aria-describedby connects input to its error message
// aria-invalid signals the invalid state to screen readers

{error && (
  
    {error}
  
)}

Complete Accessible Form

interface LoginFormProps {
  onSubmit: (email: string, password: string) => void;
}

export function LoginForm({ onSubmit }: LoginFormProps) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const newErrors: typeof errors = {};
    if (!email) newErrors.email = 'Email is required';
    if (!password) newErrors.password = 'Password is required';
    if (Object.keys(newErrors).length) {
      setErrors(newErrors);
      return;
    }
    onSubmit(email, password);
  };

  return (
    
      
        
          Email *
        
         setEmail(e.target.value)}
          aria-required="true"
          aria-describedby={errors.email ? 'email-error' : undefined}
          aria-invalid={!!errors.email}
          autoComplete="email"
        />
        {errors.email && (
          
            {errors.email}
          
        )}
      

      
        
          Password *
        
         setPassword(e.target.value)}
          aria-required="true"
          aria-describedby={errors.password ? 'password-error' : undefined}
          aria-invalid={!!errors.password}
          autoComplete="current-password"
        />
        {errors.password && (
          
            {errors.password}
          
        )}
      

      Log in
    
  );
}

Semantic HTML

Use the element that matches the intent. Screen readers and keyboard users depend on native semantics.

// BAD: div has no role, no keyboard support, no accessible name
Submit

// GOOD: button is focusable, activates on Enter/Space, announces as "button"
Submit
// BAD: non-semantic navigation
 navigate('/home')}>Home

// GOOD: anchor supports right-click, middle-click, and keyboard navigation
Home
// BAD: heading hierarchy skipped (h1 to h4)
Dashboard
Recent Activity

// GOOD: sequential heading levels
Dashboard
Recent Activity

ARIA Attributes

Use ARIA only when native HTML semantics are insufficient. Wrong ARIA is worse than no ARIA.

aria-label vs aria-labelledby

// aria-label: inline string label — use when no visible label text exists

  

// aria-labelledby: references another element's text — use when a visible label exists

  Recent Orders
  {/* content */}

aria-describedby

// Provides supplementary description beyond the label
 Delete account

This action cannot be undone.

aria-live for Dynamic Content

// Use aria-live to announce content that updates without a page reload
// polite: waits for user to finish current action before announcing
// assertive: interrupts immediately — use only for urgent errors

export function StatusMessage({ message, isError }: { message: string; isError?: boolean }) {
  return (
    
      {message}
    
  );
}

aria-expanded and aria-controls

export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);
  const contentId = useId();

  return (
    
       setIsOpen(prev => !prev)}>
        {title}
      
      
        {children}
      
    
  );
}

Keyboard Navigation

Every interactive element must be reachable and operable by keyboard alone.

Custom Dropdown

export function Dropdown({ options, onSelect }: { options: string[]; onSelect: (value: string) => void }) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(0);
  const listId = useId();

  if (!options.length) return null;

  const handleKeyDown = (e: React.KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setActiveIndex(i => Math.min(i + 1, options.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex(i => Math.max(i - 1, 0));
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        if (isOpen) onSelect(options[activeIndex]);
        setIsOpen(prev => !prev);
        break;
      case 'Escape':
        setIsOpen(false);
        break;
    }
  };

  return (
     setIsOpen(prev => !prev)}
    >
      {options[activeIndex]}
      {isOpen && (
        
          {options.map((option, index) => (
             {
                onSelect(option);
                setIsOpen(false);
              }}
            >
              {option}
            
          ))}
        
      )}
    
  );
}

Focus Management

Focus must move logically when UI state changes — especially for modals and route transitions.

Modal Focus Restoration

> This example covers initial focus and restoration. For a full focus trap (Tab/Shift+Tab cycling within the modal), use a library like focus-trap-react which handles edge cases like dynamic content and nested portals.

export function Modal({ isOpen, onClose, title, children }: { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode }) {
  const modalRef = useRef(null);
  const previousFocusRef = useRef(null);

  useEffect(() => {
    if (isOpen) {
      // Save currently focused element and move focus into modal
      previousFocusRef.current = document.activeElement as HTMLElement;
      modalRef.current?.focus();
    } else {
      // Restore focus to the element that opened the modal
      previousFocusRef.current?.focus();
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return (
     e.key === 'Escape' && onClose()}>
      {title}
      {children}
      Close
    
  );
}

Images and Icons

// BAD: decorative icon announced as unlabeled image

// GOOD: decorative image hidden from screen readers

// GOOD: meaningful image with descriptive alt text

// GOOD: icon button with accessible label

  

Reduced Motion

Respect users who have requested reduced motion in their OS settings.

export function useReducedMotion(): boolean {
  const [prefersReduced, setPrefersReduced] = useState(false);

  useEffect(() => {
    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
    setPrefersReduced(mq.matches);
    const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches);
    mq.addEventListener('change', handler);
    return () => mq.removeEventListener('change', handler);
  }, []);

  return prefersReduced;
}

// Usage
export function AnimatedCard({ children }: { children: React.ReactNode }) {
  const reduceMotion = useReducedMotion();

  return (
    
      {children}
    
  );
}

Anti-Patterns

// BAD: onClick on non-interactive element with no keyboard support
Click me

// BAD: aria-label on a div that has no role
...

// BAD: placeholder used as a substitute for label

// BAD: positive tabIndex creates unpredictable tab order
Submit

// BAD: aria-hidden on a focusable element — keyboard users get trapped
Open

// BAD: role="button" on div without keyboard handler
Submit
// Missing: tabIndex={0}, onKeyDown for Enter/Space

Checklist

Before submitting any interactive component for review:

  • [ ] Every `, , and has a connected via htmlFor/id`
  • [ ] Error messages are linked with aria-describedby and marked role="alert"
  • [ ] No onClick on ` or without role, tabIndex, and onKeyDown`
  • [ ] Icon-only buttons have aria-label
  • [ ] Decorative images use alt="" and aria-hidden="true"
  • [ ] Modals restore focus on close (for full focus trapping with Tab/Shift+Tab cycling, use a library like focus-trap-react)
  • [ ] Dynamic content updates use aria-live
  • [ ] prefers-reduced-motion is respected for animations

Related Skills

  • frontend-patterns — general React component and state patterns
  • design-system — design token and component consistency
  • motion-ui — animation patterns with accessibility considerations

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.