From harness-claude
Applies ARIA roles, states, and properties correctly to custom interactive widgets for enhanced screen reader support when native HTML semantics are insufficient.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Apply ARIA roles, states, and properties correctly to enhance assistive technology support for custom widgets
Implements WCAG 2.1/2.2 compliance, ARIA patterns, keyboard navigation, focus management, and accessibility testing for web components.
Provides step-by-step guidance, code generation, and best practices for ARIA attribute helper operations in frontend development with React, Vue, and CSS. Useful for accessibility tasks.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
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/