home / skills / doanchienthangdev / omgkit / accessibility

This skill helps you build WCAG 2.1 AA compliant interfaces by applying semantic HTML, ARIA, keyboard navigation, and focus management across components.

npx playbooks add skill doanchienthangdev/omgkit --skill accessibility

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.1 KB
---
name: implementing-accessibility
description: Claude implements WCAG 2.1 AA compliant web interfaces with proper ARIA, keyboard navigation, and screen reader support. Use when building accessible components or auditing existing UIs.
---

# Implementing Accessibility

## Quick Start

```tsx
// Accessible dialog with focus trap and ARIA
export function Dialog({ isOpen, onClose, title, children }: DialogProps) {
  const dialogRef = useRef<HTMLDivElement>(null);
  const titleId = useId();

  useEffect(() => {
    if (isOpen) {
      dialogRef.current?.focus();
      document.body.style.overflow = 'hidden';
    }
    return () => { document.body.style.overflow = ''; };
  }, [isOpen]);

  if (!isOpen) return null;
  return createPortal(
    <>
      <div className="dialog-backdrop" onClick={onClose} />
      <div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby={titleId} tabIndex={-1}>
        <h2 id={titleId}>{title}</h2>
        {children}
        <button onClick={onClose} aria-label="Close dialog">&times;</button>
      </div>
    </>,
    document.body
  );
}
```

## Features

| Feature | Description | Guide |
|---------|-------------|-------|
| Semantic HTML | Proper landmarks, headings, and native elements | `ref/semantic-structure.md` |
| ARIA Attributes | Roles, states, and properties for custom widgets | `ref/aria-patterns.md` |
| Keyboard Navigation | Tab order, roving tabindex, keyboard shortcuts | `ref/keyboard-nav.md` |
| Focus Management | Focus trapping, restoration, visible indicators | `ref/focus-management.md` |
| Live Regions | Dynamic content announcements for screen readers | `ref/live-regions.md` |
| Testing | jest-axe, Cypress accessibility, manual testing | `ref/a11y-testing.md` |

## Common Patterns

### Accessible Form Field

```tsx
export function FormField({ id, label, error, hint, required, ...props }: FormFieldProps) {
  const hintId = hint ? `${id}-hint` : undefined;
  const errorId = error ? `${id}-error` : undefined;
  const describedBy = [hintId, errorId].filter(Boolean).join(' ') || undefined;

  return (
    <div className={`form-field ${error ? 'has-error' : ''}`}>
      <label htmlFor={id}>
        {label}
        {required && <span aria-hidden="true">*</span>}
        {required && <span className="sr-only">(required)</span>}
      </label>
      {hint && <p id={hintId} className="form-hint">{hint}</p>}
      <input
        id={id}
        aria-required={required}
        aria-invalid={!!error}
        aria-describedby={describedBy}
        {...props}
      />
      {error && <p id={errorId} role="alert">{error}</p>}
    </div>
  );
}
```

### Roving Tabindex for Tab Component

```tsx
export function Tabs({ tabs }: { tabs: Tab[] }) {
  const [activeTab, setActiveTab] = useState(0);
  const tabRefs = useRef<HTMLButtonElement[]>([]);

  const handleKeyDown = (e: KeyboardEvent, index: number) => {
    let newIndex = index;
    if (e.key === 'ArrowRight') newIndex = (index + 1) % tabs.length;
    if (e.key === 'ArrowLeft') newIndex = (index - 1 + tabs.length) % tabs.length;
    if (e.key === 'Home') newIndex = 0;
    if (e.key === 'End') newIndex = tabs.length - 1;
    if (newIndex !== index) {
      e.preventDefault();
      setActiveTab(newIndex);
      tabRefs.current[newIndex]?.focus();
    }
  };

  return (
    <div>
      <div role="tablist">
        {tabs.map((tab, i) => (
          <button
            key={tab.id}
            ref={el => tabRefs.current[i] = el!}
            role="tab"
            aria-selected={activeTab === i}
            aria-controls={`panel-${tab.id}`}
            tabIndex={activeTab === i ? 0 : -1}
            onClick={() => setActiveTab(i)}
            onKeyDown={e => handleKeyDown(e, i)}
          >{tab.label}</button>
        ))}
      </div>
      {tabs.map((tab, i) => (
        <div key={tab.id} role="tabpanel" id={`panel-${tab.id}`} hidden={activeTab !== i} tabIndex={0}>
          {tab.content}
        </div>
      ))}
    </div>
  );
}
```

### Live Region Announcer

```tsx
export function useAnnounce() {
  const [message, setMessage] = useState('');

  const announce = useCallback((text: string) => {
    setMessage('');
    requestAnimationFrame(() => setMessage(text));
    setTimeout(() => setMessage(''), 1000);
  }, []);

  const Announcer = () => (
    <div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
      {message}
    </div>
  );

  return { announce, Announcer };
}

// Usage: announce('3 results found');
```

## Best Practices

| Do | Avoid |
|----|-------|
| Use semantic HTML elements (`<button>`, `<nav>`, `<main>`) | Removing focus outlines without replacement |
| Provide text alternatives for images | Relying on color alone to convey information |
| Ensure 4.5:1 color contrast for text | Using placeholder as the only label |
| Associate labels with form controls | Trapping keyboard focus unintentionally |
| Provide skip links for keyboard users | Auto-playing media with sound |
| Test with actual screen readers (NVDA, VoiceOver) | Using ARIA when native HTML suffices |
| Announce dynamic content changes with live regions | Very small touch targets (min 44x44px) |

Overview

This skill implements WCAG 2.1 AA–compliant web interfaces with correct ARIA usage, keyboard navigation, and screen reader support. It provides reusable patterns and components—dialogs, forms, tabs, and live-region announcers—designed to work across assistive technologies. Use it to build new accessible UI components or audit and remediate existing interfaces quickly.

How this skill works

The skill supplies ready-made component patterns with semantic HTML, ARIA roles and properties, focus management, and keyboard handling. It includes focus trapping and restoration for overlays, roving tabindex for composite widgets, aria-live announcements for dynamic updates, and accessibility-focused form controls. Testing guidance and examples for integration with jest-axe and end-to-end tools are included to validate behavior.

When to use it

  • Building modal dialogs, popovers, or other overlays that must trap focus and announce state.
  • Creating composite widgets (tabs, menus, lists) that require custom keyboard navigation and ARIA roles.
  • Implementing accessible form controls with labels, hints, and error handling tied to screen readers.
  • Adding dynamic updates that need polite or assertive announcements via live regions.
  • Auditing an existing UI for WCAG 2.1 AA gaps and applying concrete component fixes.

Best practices

  • Prefer native semantic elements before adding ARIA roles and properties.
  • Manage focus explicitly: trap on open, restore on close, and keep visible focus indicators.
  • Use roving tabindex for keyboard-controlled lists and ensure Home/End and arrow key support.
  • Provide meaningful text alternatives, maintain at least 4.5:1 contrast for body text, and avoid color-only cues.
  • Use aria-describedby and role="alert" for form hints and errors; test with actual screen readers.

Example use cases

  • Accessible dialog component with focus trap and aria-labelledby for a signup flow.
  • Tab component implementing roving tabindex and keyboard navigation for content sections.
  • Form field component that wires label, hint, and error text via aria-describedby and role alerts.
  • useAnnounce hook to notify screen reader users about search results or async updates.
  • Regression test suite integrating jest-axe and Cypress checks for accessibility regressions.

FAQ

Do I always need ARIA attributes?

No. Prefer native HTML semantics first. Add ARIA only when native elements cannot express the required behavior.

How should I test these components with screen readers?

Combine automated tools (jest-axe, Lighthouse) with manual testing in NVDA and VoiceOver, including keyboard-only interaction and live-region announcements.