From harness-claude
Applies ARIA roles, states, and properties correctly to enhance assistive technology support for custom widgets. Includes patterns for accessible names, live regions, and state attributes.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:a11y-aria-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Apply ARIA roles, states, and properties correctly to enhance assistive technology support for custom widgets
Apply ARIA roles, states, and properties correctly to enhance assistive technology support for custom widgets
Follow the first rule of ARIA: do not use ARIA if native HTML works. A <button> is better than <div role="button">. A <nav> is better than <div role="navigation">. ARIA overrides semantics; native HTML provides them for free.
Use aria-label and aria-labelledby to provide accessible names. Every interactive element needs an accessible name — screen readers announce it.
// aria-label — inline text label (when no visible label exists)
<button aria-label="Close dialog">
<XIcon />
</button>
// aria-labelledby — reference a visible label element
<h2 id="dialog-title">Confirm Deletion</h2>
<div role="dialog" aria-labelledby="dialog-title">
aria-labelledby over aria-label when a visible text element exists — it avoids translation gaps.aria-label replaces the element's visible text for screen readers. If the button says "X", aria-label="Close" makes screen readers say "Close button."aria-describedby for supplementary information. Unlike aria-labelledby (which names the element), aria-describedby provides additional context read after the name.<input
type="password"
aria-label="Password"
aria-describedby="password-help"
/>
<p id="password-help">Must be at least 8 characters with one number.</p>
aria-live regions to announce dynamic content. When content updates without a page reload (toast notifications, form validation, live scores), use aria-live to announce the change.// polite — waits for the screen reader to finish current speech
<div aria-live="polite" role="status">
{statusMessage}
</div>
// assertive — interrupts current speech (use sparingly)
<div aria-live="assertive" role="alert">
{errorMessage}
</div>
role="status" for informational updates and role="alert" for urgent errors.// Expandable section
<button
aria-expanded={isOpen}
aria-controls="panel-1"
onClick={() => setIsOpen(!isOpen)}
>
Settings
</button>
<div id="panel-1" hidden={!isOpen}>
{/* panel content */}
</div>
// Toggle button
<button aria-pressed={isMuted} onClick={toggleMute}>
Mute
</button>
// Disabled state
<button aria-disabled={isSubmitting} onClick={isSubmitting ? undefined : handleSubmit}>
Submit
</button>
aria-hidden="true" to hide decorative or redundant content from screen readers. Icons next to text labels, decorative images, and duplicate content should be hidden.<button>
<SearchIcon aria-hidden="true" />
<span>Search</span>
</button>
Do not use aria-hidden="true" on focusable elements — it creates a confusing state where the element receives focus but is invisible to assistive technology.
// Tab interface
<div role="tablist">
<button role="tab" aria-selected={activeTab === 0} aria-controls="panel-0">Tab 1</button>
<button role="tab" aria-selected={activeTab === 1} aria-controls="panel-1">Tab 2</button>
</div>
<div role="tabpanel" id="panel-0" aria-labelledby="tab-0">Content 1</div>
// Combobox (autocomplete)
<input role="combobox" aria-expanded={isOpen} aria-controls="listbox-1" aria-activedescendant={activeOptionId} />
<ul role="listbox" id="listbox-1">
<li role="option" id="opt-1" aria-selected={selected === 'opt-1'}>Option 1</li>
</ul>
// Alert dialog
<div role="alertdialog" aria-labelledby="alert-title" aria-describedby="alert-desc">
<h2 id="alert-title">Delete Account?</h2>
<p id="alert-desc">This action cannot be undone.</p>
<button>Cancel</button>
<button>Delete</button>
</div>
aria-invalid and aria-errormessage for form validation errors.<input
aria-invalid={!!errors.email}
aria-errormessage={errors.email ? 'email-error' : undefined}
/>;
{
errors.email && (
<span id="email-error" role="alert">
{errors.email}
</span>
);
}
ARIA categories:
tab, dialog, alert, progressbar). Set once; do not change dynamically.aria-expanded, aria-selected, aria-pressed).aria-label, aria-describedby, aria-controls).The five rules of ARIA:
role="button" on an <a>).role="presentation" or aria-hidden="true" on focusable elements.Common mistakes:
role="button" without keyboard support (Enter and Space activation)aria-label on non-interactive elements where it has no effectaria-expanded without updating it when state changesaria-live="assertive" (interrupts users constantly)aria-hidden="true" on a parent containing focusable childrenTesting: Use the accessibility tree in browser DevTools to verify that ARIA attributes produce the expected accessible name, role, and state.
https://www.w3.org/TR/wai-aria-1.2/
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeAssigns valid ARIA roles, accessible names, and required states to custom UI components that native HTML cannot represent, following WAI-ARIA Authoring Practices.
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.
Implements WCAG 2.1/2.2 compliance, ARIA patterns, keyboard navigation, focus management, and accessibility testing for web components.