Agent skill

web-accessibility-patterns

Web accessibility patterns including semantic HTML, ARIA attributes, and keyboard navigation. PROACTIVELY activate for: (1) using semantic HTML elements correctly, (2) adding ARIA labels for icons and dynamic content, (3) implementing keyboard navigation. Triggers: "accessibility", "a11y", "aria"

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/web-accessibility-patterns

SKILL.md

Web Accessibility Patterns

Semantic HTML Foundation

Use correct elements:

tsx
// GOOD
<button onClick={handleClick}>Submit</button>
<nav>...</nav>
<main>...</main>
<article>...</article>
<aside>...</aside>
<header>...</header>
<footer>...</footer>

// BAD
<div onClick={handleClick}>Submit</div>
<div className="nav">...</div>

ARIA Attributes

Icon-Only Buttons

tsx
<Button variant="ghost" size="icon" aria-label="Close dialog">
  <X size={16} />
</Button>

<Button variant="ghost" size="icon" aria-label="Open menu">
  <Menu size={16} />
</Button>

Form Fields

tsx
<div>
  <Label htmlFor="email">Email</Label>
  <Input
    id="email"
    type="email"
    aria-describedby="email-description email-error"
    aria-invalid={!!error}
    aria-required="true"
  />
  <p id="email-description" className="text-sm text-muted-foreground">
    We'll never share your email
  </p>
  {error && (
    <p id="email-error" className="text-sm text-destructive" role="alert">
      {error}
    </p>
  )}
</div>

Loading States

tsx
<div aria-live="polite" aria-busy={isLoading}>
  {isLoading ? <Skeleton /> : <Content />}
</div>

<Button disabled={isLoading} aria-disabled={isLoading}>
  {isLoading ? (
    <>
      <Loader className="animate-spin" aria-hidden="true" />
      <span className="sr-only">Loading</span>
      Submitting...
    </>
  ) : (
    'Submit'
  )}
</Button>

Expandable Content

tsx
function Accordion({ title, children, isOpen, onToggle }) {
  const contentId = useId();

  return (
    <div>
      <button
        aria-expanded={isOpen}
        aria-controls={contentId}
        onClick={onToggle}
      >
        {title}
      </button>
      <div id={contentId} hidden={!isOpen}>
        {children}
      </div>
    </div>
  );
}

Keyboard Navigation

Custom Interactive Element

tsx
<div
  role="button"
  tabIndex={0}
  onClick={handleClick}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      handleClick();
    }
  }}
>
  Interactive element
</div>

Keyboard Trap for Modals

tsx
function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!isOpen) return;

    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        onClose();
      }
    };

    // Focus trap - keep focus within modal
    const focusableElements = modalRef.current?.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );

    document.addEventListener('keydown', handleKeyDown);
    focusableElements?.[0]?.focus();

    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [isOpen, onClose]);

  return (
    <div ref={modalRef} role="dialog" aria-modal="true">
      {children}
    </div>
  );
}

Focus Management

Visible Focus Styles

tsx
<Button className="
  focus-visible:outline-none
  focus-visible:ring-2
  focus-visible:ring-ring
  focus-visible:ring-offset-2
">
  Accessible focus state
</Button>

Skip Link

tsx
// Add at very top of page
<a
  href="#main-content"
  className="
    sr-only focus:not-sr-only
    focus:absolute focus:top-4 focus:left-4
    focus:z-50 focus:px-4 focus:py-2
    focus:bg-background focus:text-foreground
  "
>
  Skip to main content
</a>

// Main content target
<main id="main-content" tabIndex={-1}>
  ...
</main>

Screen Reader Only Content

tsx
// Visually hidden but announced
<span className="sr-only">Opens in new tab</span>

// Or with Tailwind
<span className="absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0">
  Screen reader only text
</span>

Color and Contrast

tsx
// Use semantic colors that have proper contrast
<p className="text-foreground">Primary text - high contrast</p>
<p className="text-muted-foreground">Secondary text - still readable</p>

// Error states
<p className="text-destructive">Error message</p>

// Don't rely on color alone
<span className="text-destructive flex items-center gap-2">
  <AlertCircle size={16} aria-hidden="true" />
  Required field
</span>

Common ARIA Roles

Role Use Case
button Clickable non-button elements
dialog Modal dialogs
alert Important messages
status Status updates
navigation Navigation sections
tablist, tab, tabpanel Tab interfaces
menu, menuitem Dropdown menus

Anti-Patterns

  • <div onClick> instead of <button>
  • Icon without aria-label
  • Redundant aria-label on elements with text
  • Missing keyboard support for custom interactive elements
  • Removing focus outlines without replacement
  • Color as only indicator of state

Best Practices

  • Semantic HTML first
  • ARIA for icons and dynamic content
  • Keyboard navigation for all interactive elements
  • Focus visible styles
  • Proper heading hierarchy (h1 -> h2 -> h3)
  • Sufficient color contrast (4.5:1 for text)
  • Descriptive link text (not "click here")

Related Skills: shadcn-component-scaffolding, framer-motion-interactive-animation

Didn't find tool you were looking for?

Be as detailed as possible for better results