home / skills / hoodini / ai-agents-skills / web-accessibility

web-accessibility skill

/skills/web-accessibility

This skill helps you build accessible web applications by applying WCAG and ARIA patterns, ensuring keyboard navigation and screen reader support.

npx playbooks add skill hoodini/ai-agents-skills --skill web-accessibility

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

Files (1)
SKILL.md
4.8 KB
---
name: web-accessibility
description: Build accessible web applications following WCAG guidelines. Use when implementing ARIA patterns, keyboard navigation, screen reader support, or ensuring accessibility compliance. Triggers on accessibility, a11y, WCAG, ARIA, screen reader, keyboard navigation.
---

# Web Accessibility (WCAG 2.1)

Build accessible web applications that work for everyone.

## ARIA Patterns

### Button
```tsx
<button
  type="button"
  aria-pressed={isPressed}
  aria-disabled={isDisabled}
  onClick={handleClick}
>
  Toggle Feature
</button>
```

### Modal Dialog
```tsx
<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
  aria-describedby="modal-description"
>
  <h2 id="modal-title">Confirm Action</h2>
  <p id="modal-description">Are you sure you want to proceed?</p>
  <button onClick={onConfirm}>Confirm</button>
  <button onClick={onCancel}>Cancel</button>
</div>
```

### Navigation Menu
```tsx
<nav aria-label="Main navigation">
  <ul role="menubar">
    <li role="none">
      <a role="menuitem" href="/home">Home</a>
    </li>
    <li role="none">
      <button
        role="menuitem"
        aria-haspopup="true"
        aria-expanded={isOpen}
      >
        Products
      </button>
      {isOpen && (
        <ul role="menu" aria-label="Products submenu">
          <li role="none">
            <a role="menuitem" href="/products/new">New</a>
          </li>
        </ul>
      )}
    </li>
  </ul>
</nav>
```

## Keyboard Navigation

### Focus Management
```tsx
import { useEffect, useRef } from 'react';

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

  useEffect(() => {
    if (isOpen) {
      previousFocus.current = document.activeElement as HTMLElement;
      modalRef.current?.focus();
    } else {
      previousFocus.current?.focus();
    }
  }, [isOpen]);

  // Trap focus within modal
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Escape') {
      onClose();
    }
    
    if (e.key === 'Tab') {
      const focusable = modalRef.current?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      
      if (focusable && focusable.length > 0) {
        const first = focusable[0] as HTMLElement;
        const last = focusable[focusable.length - 1] as HTMLElement;
        
        if (e.shiftKey && document.activeElement === first) {
          e.preventDefault();
          last.focus();
        } else if (!e.shiftKey && document.activeElement === last) {
          e.preventDefault();
          first.focus();
        }
      }
    }
  };

  if (!isOpen) return null;

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

## Color Contrast

Minimum contrast ratios (WCAG AA):
- Normal text: 4.5:1
- Large text (18pt+): 3:1
- UI components: 3:1

```typescript
function getContrastRatio(color1: string, color2: string): number {
  const lum1 = getLuminance(color1);
  const lum2 = getLuminance(color2);
  const lighter = Math.max(lum1, lum2);
  const darker = Math.min(lum1, lum2);
  return (lighter + 0.05) / (darker + 0.05);
}

function getLuminance(hex: string): number {
  const rgb = hexToRgb(hex);
  const [r, g, b] = rgb.map((c) => {
    c = c / 255;
    return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
```

## Accessible Forms

```tsx
<form onSubmit={handleSubmit}>
  <div>
    <label htmlFor="email">
      Email address
      <span aria-hidden="true">*</span>
      <span className="sr-only">(required)</span>
    </label>
    <input
      id="email"
      type="email"
      aria-required="true"
      aria-invalid={errors.email ? 'true' : 'false'}
      aria-describedby={errors.email ? 'email-error' : undefined}
    />
    {errors.email && (
      <p id="email-error" role="alert" className="error">
        {errors.email}
      </p>
    )}
  </div>
  
  <button type="submit">Submit</button>
</form>
```

## Screen Reader Only Content

```css
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
```

## Testing

```bash
# Automated testing
npm install -D axe-core @axe-core/react

# In tests
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

test('component is accessible', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
```

## Resources

- **WCAG 2.1 Guidelines**: https://www.w3.org/WAI/WCAG21/quickref/
- **ARIA Authoring Practices**: https://www.w3.org/WAI/ARIA/apg/

Overview

This skill helps you build accessible web applications that follow WCAG 2.1 and ARIA best practices. It provides patterns, code examples, and testing guidance for keyboard navigation, screen reader support, color contrast, and accessible forms. Use it to reduce accessibility defects and improve inclusive UX.

How this skill works

I provide concrete ARIA patterns (buttons, modals, navigation), focus management examples for keyboard users, and techniques for contrast calculation and screen-reader-only content. The skill includes form semantics, validation ARIA attributes, and automated testing examples with axe. Follow the patterns to ensure assistive technologies can interact reliably with your UI.

When to use it

  • Implementing ARIA roles and properties for interactive components
  • Building modal dialogs, menus, or custom widgets requiring focus management
  • Ensuring keyboard-only users can navigate and operate your UI
  • Verifying color contrast and visual readability against WCAG ratios
  • Adding accessible form labels, error messaging, and required indicators

Best practices

  • Prefer native semantics before adding ARIA; use role only when needed
  • Manage focus: restore previous focus on close and trap focus inside modals
  • Use aria-describedby and role="alert" for clear, programmatic error messages
  • Ensure contrast meets WCAG AA (4.5:1 normal text, 3:1 large text/components)
  • Test with automated tools (axe) and manual assistive-tech testing (screen readers, keyboard)

Example use cases

  • Create an accessible modal that traps focus, supports Escape to close, and restores prior focus
  • Build a keyboard-navigable menubar with proper aria-haspopup and aria-expanded states
  • Enhance form UX with aria-required, aria-invalid, and descriptive error blocks announced to screen readers
  • Calculate and validate foreground/background contrast programmatically during design and CI checks
  • Hide visually redundant text with a screen-reader-only class for clearer announcements

FAQ

Do I always need ARIA?

No. Use native HTML elements first (button, nav, form controls). Apply ARIA only to fill gaps where semantics are missing.

How do I test keyboard accessibility?

Manually tab through the interface, verify logical focus order and focus indicators, and run automated checks (axe) plus screen reader verification.