AgentStack
SKILL verified MIT Self-run

Accessibility Patterns

skill-marvinrichter-clarc-accessibility-patterns · by marvinrichter

Web Accessibility (WCAG 2.2): key success criteria (contrast, keyboard, focus, ARIA), ARIA patterns (aria-label/labelledby/describedby, roles, live regions, state attributes), keyboard navigation (focus trap, skip links, roving tabindex), screen reader testing (NVDA/VoiceOver/TalkBack), automated testing (axe-core, axe-playwright, jest-axe), and accessible component patterns (modals, tables, form…

No reviews yet
0 installs
18 views
0.0% view→install

Install

$ agentstack add skill-marvinrichter-clarc-accessibility-patterns

✓ 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.

Are you the author of Accessibility Patterns? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Accessibility Patterns

Build accessible UIs that work for all users — keyboard, screen readers, and beyond.

When to Activate

  • Building or reviewing interactive components (modals, forms, dropdowns, tabs)
  • Running accessibility audits on existing code
  • Adding automated a11y tests with axe-core or jest-axe
  • Implementing keyboard navigation or focus management
  • Checking WCAG 2.2 compliance
  • Adding ARIA attributes to custom components

WCAG 2.2 — Key Success Criteria

| Criterion | Level | What it means | |-----------|-------|---------------| | 1.1.1 Non-text Content | A | All images need alt text or aria-label | | 1.3.1 Info and Relationships | A | Use semantic HTML — headings, lists, landmarks | | 1.4.3 Contrast Minimum | AA | Text: 4.5:1 ratio; large text (≥18pt): 3:1 | | 1.4.11 Non-text Contrast | AA | UI components and graphics: 3:1 ratio | | 2.1.1 Keyboard | A | Everything operable via keyboard | | 2.4.3 Focus Order | A | Tab order is logical and meaningful | | 2.4.7 Focus Visible | AA | Keyboard focus is always visible | | 2.4.11 Focus Appearance | AA | (WCAG 2.2 new) Focus indicator: ≥2px, ≥3:1 contrast | | 2.5.8 Target Size | AA | (WCAG 2.2 new) Touch targets ≥24×24 CSS px | | 4.1.2 Name, Role, Value | A | ARIA attributes are correct and complete |


ARIA Patterns

Labels


  ...

  Confirm Delete
  ...

Use your company email address.
This field is required.

Roles


Custom Button

...      
...        
...  
... 
... 

  

Live Regions


  
  3 results found

  Error: Payment failed. Please try again.

Saving...

State Attributes


Menu
...

Bold

Details

Email address is required.

  

Keyboard Navigation

Focus Trap (Modals/Drawers)

// Focus trap — keep focus inside modal while open
function trapFocus(container: HTMLElement): () => void {
  const focusableSelectors = [
    'a[href]',
    'button:not([disabled])',
    'input:not([disabled])',
    'select:not([disabled])',
    'textarea:not([disabled])',
    '[tabindex]:not([tabindex="-1"])',
  ].join(', ');

  const focusableElements = Array.from(
    container.querySelectorAll(focusableSelectors)
  );
  const firstElement = focusableElements[0];
  const lastElement = focusableElements.at(-1)!;

  // Move focus into modal on open
  firstElement?.focus();

  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key !== 'Tab') return;

    if (e.shiftKey) {
      // Shift+Tab: if on first element, wrap to last
      if (document.activeElement === firstElement) {
        e.preventDefault();
        lastElement.focus();
      }
    } else {
      // Tab: if on last element, wrap to first
      if (document.activeElement === lastElement) {
        e.preventDefault();
        firstElement.focus();
      }
    }
  };

  container.addEventListener('keydown', handleKeyDown);
  return () => container.removeEventListener('keydown', handleKeyDown);
}

// React hook
function useFocusTrap(isOpen: boolean, containerRef: RefObject) {
  useEffect(() => {
    if (!isOpen || !containerRef.current) return;

    const previouslyFocused = document.activeElement as HTMLElement;
    const cleanup = trapFocus(containerRef.current);

    return () => {
      cleanup();
      // Restore focus when modal closes
      previouslyFocused?.focus();
    };
  }, [isOpen]);
}

Skip Links


Skip to main content

  
  ...
/* Visible only when focused */
.skip-link {
  position: absolute;
  top: -40px;
  left: 6px;
  z-index: 9999;
  padding: 8px;
  background: #000;
  color: #fff;
  text-decoration: none;
}
.skip-link:focus {
  top: 6px;
}

Focus Management on Route Change (SPA)

// Restore focus to page heading on every route change
function RouterFocusManager() {
  const pathname = usePathname();
  const headingRef = useRef(null);

  useEffect(() => {
    headingRef.current?.focus();
  }, [pathname]);

  return {getPageTitle(pathname)};
}

WAI-ARIA Menu Button (Dropdowns)

// Full keyboard support: Enter/Space/ArrowDown opens, ArrowUp/Down/Home/End navigate, Escape closes
function DropdownMenu({ trigger, items }: DropdownMenuProps) {
  const [open, setOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);

  const handleKeyDown = (e: KeyboardEvent) => {
    if (!open) {
      if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
        e.preventDefault();
        setOpen(true);
        setActiveIndex(0);
      }
      return;
    }
    switch (e.key) {
      case 'ArrowDown': e.preventDefault(); setActiveIndex(i => Math.min(i + 1, items.length - 1)); break;
      case 'ArrowUp':   e.preventDefault(); setActiveIndex(i => Math.max(i - 1, 0)); break;
      case 'Home':      e.preventDefault(); setActiveIndex(0); break;
      case 'End':       e.preventDefault(); setActiveIndex(items.length - 1); break;
      case 'Escape':    setOpen(false); break;
      case 'Tab':       setOpen(false); break;
    }
  };

  return (
    
       setOpen(!open)}>
        Options
      
      {open && (
        
          {items.map((item, i) => (
             { item.onSelect(); setOpen(false); }}>
              {item.label}
            
          ))}
        
      )}
    
  );
}

Roving tabindex (Radio Groups, Toolbars)

// Only one item in group has tabindex="0" at a time
// Arrow keys move focus within the group
function RovingTabGroup({ items }: { items: string[] }) {
  const [activeIndex, setActiveIndex] = useState(0);

  const handleKeyDown = (e: KeyboardEvent, index: number) => {
    let next = index;

    if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
      next = (index + 1) % items.length;
    } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
      next = (index - 1 + items.length) % items.length;
    } else if (e.key === 'Home') {
      next = 0;
    } else if (e.key === 'End') {
      next = items.length - 1;
    } else {
      return;
    }

    e.preventDefault();
    setActiveIndex(next);
    itemRefs[next]?.focus();
  };

  return (
    
      {items.map((item, i) => (
         handleKeyDown(e, i)}
          ref={(el) => { itemRefs[i] = el; }}
        >
          {item}
        
      ))}
    
  );
}

Escape Key Convention

// All overlays (modals, drawers, dropdowns, tooltips) must close on Escape
useEffect(() => {
  const handleEscape = (e: KeyboardEvent) => {
    if (e.key === 'Escape' && isOpen) {
      onClose();
    }
  };
  document.addEventListener('keydown', handleEscape);
  return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen, onClose]);

For screen reader testing checklists (NVDA/VoiceOver), automated testing (axe-core, jest-axe, @axe-core/react), and accessible component patterns (modal, table, form, data visualization), see skill accessibility-patterns-advanced.

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.