# Frontend A11y

> >

- **Type:** Skill
- **Install:** `agentstack add skill-affaan-m-ecc-frontend-a11y`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [affaan-m](https://agentstack.voostack.com/s/affaan-m)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [affaan-m](https://github.com/affaan-m)
- **Source:** https://github.com/affaan-m/ECC/tree/main/skills/frontend-a11y
- **Website:** https://ecc.tools

## Install

```sh
agentstack add skill-affaan-m-ecc-frontend-a11y
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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

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

// GOOD: htmlFor matches input id
Email

```

### Required Fields

```tsx
// 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

```tsx
// 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

```tsx
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.

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

// GOOD: button is focusable, activates on Enter/Space, announces as "button"
Submit
```

```tsx
// BAD: non-semantic navigation
 navigate('/home')}>Home

// GOOD: anchor supports right-click, middle-click, and keyboard navigation
Home
```

```tsx
// 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

```tsx
// 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

```tsx
// Provides supplementary description beyond the label
 Delete account

This action cannot be undone.
```

### aria-live for Dynamic Content

```tsx
// 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

```tsx
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

```tsx
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`](https://github.com/focus-trap/focus-trap-react) which handles edge cases like dynamic content and nested portals.

```tsx
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

```tsx
// 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.

```tsx
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

```tsx
// 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.

- **Author:** [affaan-m](https://github.com/affaan-m)
- **Source:** [affaan-m/ECC](https://github.com/affaan-m/ECC)
- **License:** MIT
- **Homepage:** https://ecc.tools

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

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-affaan-m-ecc-frontend-a11y
- Seller: https://agentstack.voostack.com/s/affaan-m
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
