Claude-skill-registry aria-patterns
Provides ARIA roles, states, and properties for interactive components. Use when building custom widgets, fixing screen reader issues, or implementing modals, tabs, accordions, menus, or dialogs accessibly.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/aria-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-aria-patterns && rm -rf "$T"
manifest:
skills/data/aria-patterns/SKILL.mdsource content
ARIA Patterns Guide
Overview
Implement accessible interactive components using correct ARIA roles, states, and properties. Provides copy-paste patterns for common widgets that work with screen readers and keyboard navigation.
When to Use
- Building custom interactive components
- Making dynamic content accessible
- Fixing screen reader issues
- Adding keyboard support to custom widgets
Quick Reference: First Rules of ARIA
- Don't use ARIA if native HTML works -
over<button><div role="button"> - Don't change native semantics - Don't put
on a headingrole="button" - All interactive ARIA elements must be keyboard accessible
- Don't use
orrole="presentation"
on focusable elementsaria-hidden="true" - All interactive elements must have accessible names
The Process
- Identify component type: What widget pattern matches?
- Check native HTML first: Can a semantic element do this?
- Apply ARIA pattern: Roles, states, properties
- Add keyboard support: Expected keys for the pattern
- Test with screen reader: Verify announcements
Component Patterns
Button
Native (preferred):
<button type="button">Click me</button>
Custom (when necessary):
<div role="button" tabindex="0" aria-pressed="false" onkeydown="handleKeyDown(event)" > Toggle </div> <script> function handleKeyDown(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.target.click(); } } </script>
Toggle Button
<button type="button" aria-pressed="false" onclick="this.setAttribute('aria-pressed', this.getAttribute('aria-pressed') === 'true' ? 'false' : 'true')" > <span class="sr-only">Enable</span> Dark Mode </button>
Modal Dialog
<div role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-desc" > <h2 id="modal-title">Confirm Action</h2> <p id="modal-desc">Are you sure you want to proceed?</p> <button type="button">Cancel</button> <button type="button">Confirm</button> </div>
Required behavior:
- Focus moves to dialog on open
- Focus trapped within dialog
- Escape key closes dialog
- Focus returns to trigger on close
// Focus trap example function trapFocus(dialog) { const focusable = dialog.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const first = focusable[0]; const last = focusable[focusable.length - 1]; dialog.addEventListener('keydown', (e) => { if (e.key === 'Tab') { if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } } }); }
Dropdown Menu
<div class="dropdown"> <button type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="dropdown-menu" id="dropdown-trigger" > Options </button> <ul role="menu" id="dropdown-menu" aria-labelledby="dropdown-trigger" hidden > <li role="none"> <button role="menuitem" tabindex="-1">Edit</button> </li> <li role="none"> <button role="menuitem" tabindex="-1">Duplicate</button> </li> <li role="none"> <button role="menuitem" tabindex="-1">Delete</button> </li> </ul> </div>
Keyboard:
- Enter/Space: Open menu, activate item
- Arrow Down: Next item (or first if closed)
- Arrow Up: Previous item
- Escape: Close menu
- Home: First item
- End: Last item
Tabs
<div class="tabs"> <div role="tablist" aria-label="Account settings"> <button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0" > Profile </button> <button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1" > Security </button> <button role="tab" aria-selected="false" aria-controls="panel-3" id="tab-3" tabindex="-1" > Billing </button> </div> <div role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0"> Profile content... </div> <div role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0" hidden> Security content... </div> <div role="tabpanel" id="panel-3" aria-labelledby="tab-3" tabindex="0" hidden> Billing content... </div> </div>
Keyboard:
- Arrow Left/Right: Move between tabs
- Home: First tab
- End: Last tab
- Tab: Move into panel content
Accordion
<div class="accordion"> <h3> <button type="button" aria-expanded="true" aria-controls="section-1" id="accordion-header-1" > Section 1 </button> </h3> <div role="region" id="section-1" aria-labelledby="accordion-header-1" > Section 1 content... </div> <h3> <button type="button" aria-expanded="false" aria-controls="section-2" id="accordion-header-2" > Section 2 </button> </h3> <div role="region" id="section-2" aria-labelledby="accordion-header-2" hidden > Section 2 content... </div> </div>
Tooltip
<button type="button" aria-describedby="tooltip-1" > Help </button> <div role="tooltip" id="tooltip-1" hidden > Click here for more information </div>
Note: For interactive content, use a disclosure or dialog instead.
Alert / Status Messages
<!-- Alert (important, interruptive) --> <div role="alert"> Error: Please enter a valid email address. </div> <!-- Status (polite update) --> <div role="status" aria-live="polite"> 3 items in cart </div> <!-- Live region (for dynamic content) --> <div aria-live="polite" aria-atomic="true"> <!-- Content updates announced to screen readers --> </div>
Combobox (Autocomplete)
<div class="combobox"> <label for="search-input">Search</label> <input type="text" id="search-input" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="search-listbox" aria-activedescendant="" > <ul role="listbox" id="search-listbox" hidden > <li role="option" id="option-1">Option 1</li> <li role="option" id="option-2">Option 2</li> <li role="option" id="option-3" aria-selected="true">Option 3</li> </ul> </div>
Update
to the ID of the highlighted option.aria-activedescendant
Progress / Loading
<!-- Determinate progress --> <div role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100" aria-label="Upload progress" > 75% </div> <!-- Indeterminate loading --> <div role="status" aria-label="Loading" > <span class="spinner" aria-hidden="true"></span> <span class="sr-only">Loading...</span> </div>
Common ARIA Attributes
| Attribute | Purpose | Example |
|---|---|---|
| Accessible name | |
| Name from element | |
| Description | |
| Open/closed state | |
| Controlled element | |
| Hide from AT | |
| Announce updates | |
| Toggle state | |
| Selection state | |
| Current item | |
Screen Reader Only Text
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; }
Testing
- Keyboard only: Tab through, use arrows, Enter, Escape
- Screen reader: Test with VoiceOver (Mac), NVDA (Windows), or JAWS
- Check announcements: Are labels, states, and changes announced?
- axe DevTools: Run automated accessibility audit