Skip to content
Back to skills

Apply Aria Roles

ASecurity

Use when building custom UI components that native HTML cannot represent — to give assistive technology the correct role, state, and property information for non-native interactive elements.

  • 4 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added September 8, 2026
ai-agentsjavascriptjavareactangularawstestinggitsecurity

Works with

  • cli

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add jeffreytse/grimoire-core --skill apply-aria-roles --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Apply Aria Roles?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Apply Aria Roles
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/jeffreytse-apply-aria-roles/badge)](https://www.skillsdirectory.com/skills/jeffreytse-apply-aria-roles)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: apply-aria-roles
description: Use when building custom UI components that native HTML cannot represent — to give assistive technology the correct role, state, and property information for non-native interactive elements.
source: W3C WCAG 2.1 SC 4.1.2 Name, Role, Value (Level A); WAI-ARIA 1.2 specification; WAI-ARIA Authoring Practices 1.2; MDN ARIA guide
tags: [accessibility, wcag, a11y, aria, roles, states, properties, screen-reader]
related: [apply-keyboard-accessibility, write-semantic-html-structure, design-accessibility-standards]
---

# Apply ARIA Roles

Give custom UI components a valid ARIA role, accessible name, and required states — so screen readers understand and announce them correctly.

## Why This Is Best Practice

**Adopted by:** WCAG 2.1 SC 4.1.2 (Level A) is the robustness principle — required by
all accessibility laws. WAI-ARIA 1.2 is the W3C specification implemented in all modern
screen readers (NVDA, JAWS, VoiceOver, TalkBack). React Aria (Adobe), Headless UI
(Tailwind), Radix UI, and Angular CDK all implement WAI-ARIA patterns — used in millions
of production applications.
**Impact:** Deque Systems research found custom widgets without ARIA account for 38% of
critical accessibility defects in enterprise applications. A `<div>` with `onclick` and
no ARIA is announced as nothing (or as the text content) — users cannot determine the
element is interactive. ARIA provides the "what is it" signal that screen readers need
to announce controls correctly.
**Why best:** The alternative — styling `<div>` elements as buttons and hoping screen
readers infer their purpose — is unreliable. ARIA provides an explicit contract:
"this element is a tab", "this element is expanded", "this element is required".

Sources: W3C WCAG 2.1 SC 4.1.2 (2018); WAI-ARIA 1.2 (2023); WAI-ARIA Authoring
Practices 1.2; Deque Systems 2024 State of Digital Accessibility

## Steps

### Step 1: Follow the First Rule of ARIA — use native HTML first

```html
<!-- Wrong — custom button needs manual ARIA + keyboard + focus -->
<div role="button" tabindex="0" onclick="submit()">Submit</div>

<!-- Right — native button has all of this by default -->
<button type="submit">Submit</button>
```

WAI-ARIA First Rule: "If you can use a native HTML element or attribute with the
semantics and behavior you require already built in, instead of re-purposing an element
and adding an ARIA role, state or property to make it accessible, then do so."

### Step 2: Match role to the component pattern exactly

Use WAI-ARIA Authoring Practices (w3.org/WAI/ARIA/apg) as the source of truth.
Common patterns:

```html
<!-- Tab panel -->
<div role="tablist" aria-label="Account settings">
  <button role="tab" aria-selected="true" aria-controls="panel-profile" id="tab-profile">Profile</button>
  <button role="tab" aria-selected="false" aria-controls="panel-security" id="tab-security">Security</button>
</div>
<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">...</div>
<div role="tabpanel" id="panel-security" aria-labelledby="tab-security" hidden>...</div>

<!-- Accordion -->
<h3>
  <button aria-expanded="true" aria-controls="section-1">Section 1</button>
</h3>
<div id="section-1" role="region" aria-labelledby="...">...</div>

<!-- Alert dialog (requires immediate focus) -->
<div role="alertdialog" aria-modal="true" aria-labelledby="dlg-title" aria-describedby="dlg-desc">
  <h2 id="dlg-title">Delete account</h2>
  <p id="dlg-desc">This action cannot be undone.</p>
  <button>Cancel</button>
  <button>Delete</button>
</div>
```

### Step 3: Provide accessible names for all interactive elements

Every interactive element needs an accessible name via one of these methods (in priority order):
1. `aria-labelledby` — references visible text in the DOM
2. `<label>` — for form inputs
3. `aria-label` — for elements with no visible text label
4. `title` — last resort, not reliably announced by all AT

```html
<!-- Icon button — no visible text, needs aria-label -->
<button aria-label="Delete item">
  <svg aria-hidden="true" focusable="false">...</svg>
</button>

<!-- Region labeled by heading -->
<section aria-labelledby="search-heading">
  <h2 id="search-heading">Search results</h2>
  ...
</section>
```

### Step 4: Update ARIA states when UI changes

States must be kept in sync with the UI. Stale states confuse screen readers.

```javascript
// Accordion expand/collapse
function toggleAccordion(button, panel) {
  const expanded = button.getAttribute('aria-expanded') === 'true';
  button.setAttribute('aria-expanded', String(!expanded));
  panel.hidden = expanded;
}

// Loading state
function startLoading(button) {
  button.setAttribute('aria-busy', 'true');
  button.setAttribute('aria-label', 'Saving…');
}

function stopLoading(button) {
  button.removeAttribute('aria-busy');
  button.setAttribute('aria-label', 'Save');
}
```

Common states to keep in sync: `aria-expanded`, `aria-selected`, `aria-checked`,
`aria-disabled`, `aria-invalid`, `aria-busy`.

### Step 5: Validate ARIA against the specification — avoid invalid combinations

```html
<!-- Wrong — aria-required on a div is invalid (not a form field role) -->
<div role="button" aria-required="true">Submit</div>

<!-- Wrong — role="presentation" on a focusable element -->
<a href="/home" role="presentation">Home</a>   <!-- removes link semantics -->

<!-- Wrong — conflicting roles -->
<button role="link">Click</button>   <!-- button IS a role; overriding with link is valid
                                          but confusing — just use <a href> -->
```

Use the axe-core browser extension to validate ARIA — it flags invalid role/attribute
combinations that are invisible to manual testing.

## When NOT to Use

- **When a native element exists** — never use `role="button"` when `<button>` is available. Never use `role="checkbox"` when `<input type="checkbox">` is available.
- **`aria-hidden` on focusable elements** — never hide a focusable element with `aria-hidden="true"`. It removes the element from the AT tree while leaving it in the Tab order, creating a "ghost" interactive element.

## Common Mistakes

**ARIA without keyboard support.** `role="button"` makes an element look like a button to screen readers — but without `tabindex="0"` and keyboard event handlers, it's not keyboard operable. Role + keyboard support are inseparable.

**`aria-label` on non-interactive elements.** `aria-label` on a `<div>` with no role has no effect. ARIA attributes only apply to elements with an explicit or implicit ARIA role.

**Not hiding decorative SVGs from AT.** An inline SVG without `aria-hidden="true"` and `focusable="false"` will be announced by some screen readers. Decorative SVGs must be hidden: `<svg aria-hidden="true" focusable="false">`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…