aria-patterns
Skill50 starsChanged 11 months agoMITView on GitHub
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.
In this repository29 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- color-contrastSkill
- focus-statesSkill
- component-docsSkill
- token-docsSkill
- angularSkill
- reactSkill
- svelteSkill
- vueSkill
- getting-startedSkill
- animation-principlesSkill
- compound-componentsSkill
- dark-modeSkill
- icon-systemSkill
- layout-primitivesSkill
- breakpointsSkill
- color-scaleSkill
- design-tokens-structureSkill
- motion-scaleSkill
- radius-scaleSkill
- responsive-typographySkill
- shadow-scaleSkill
- spacing-scaleSkill
- type-scaleSkill
- z-index-scaleSkill
- figmaSkill
- framerSkill
- storybookSkill
- style-dictionarySkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 3 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 22
Don't change native semantics: - Don't put role="button" on a heading
- Distinctive: line 33
Test with screen reader: Verify announcements
- Distinctive: line 404
Screen reader: Test with VoiceOver (Mac), NVDA (Windows), or JAWS
- Distinctive: line 406
axe DevTools: Run automated accessibility audit
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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 -
<button>over<div role="button"> - Don't change native semantics - Don't put
role="button"on a heading - All interactive ARIA elements must be keyboard accessible
- Don't use
role="presentation"oraria-hidden="true"on focusable elements - 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 aria-activedescendant to the ID of the highlighted option.
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 |
aria-label | Accessible name | aria-label="Close" |
aria-labelledby | Name from element | aria-labelledby="heading-1" |
aria-describedby | Description | aria-describedby="hint-1" |
aria-expanded | Open/closed state | aria-expanded="true" |
aria-controls | Controlled element | aria-controls="menu-1" |
aria-hidden | Hide from AT | aria-hidden="true" |
aria-live | Announce updates | aria-live="polite" |
aria-pressed | Toggle state | aria-pressed="false" |
aria-selected | Selection state | aria-selected="true" |
aria-current | Current item | aria-current="page" |
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
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

