---
name: aria-names-roles
description: "Audits and fixes accessible names and ARIA roles in HTML: buttons, links and fields with no name, aria-label on divs and spans, aria-labelledby pointing nowhere, redundant, invalid or abstract roles, missing required ARIA attributes, aria-hidden on focusable content, label-in-name mismatches (WCAG 4.1.2, 2.5.3). Runs a static checker, then reads the browser's computed names. Use for axe button-name, label or aria-allowed-role errors. Do NOT use for contrast, focus order, live regions or screen reader testing."
license: CC-BY-4.0
metadata:
  author: Afeez Adebayo
  author_url: https://afeezadebayo.com
  author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
  author_x: https://x.com/za_icon
  source: https://afeezadebayo.com/skills-md/frontend-development/accessibility/aria-names-roles/
  category: frontend-development
  subcategory: accessibility
  version: "1.0.0"
  created: "2026-10-30"
  updated: "2026-10-30"
---

# ARIA Names and Roles: Fix Unnamed Controls and Misused Roles

An agent works on names and roles in this order: run a read-only static checker over the rendered HTML, read the browser's computed role and name for every interactive element, fix with native HTML first, then `aria-labelledby` or `aria-label`, then roles and required attributes, and re-run both tools. The checker finds what the markup says. The browser read shows what a screen reader will be given.

Lookup tables are in `references/names-and-roles.md`. Measurements and axe rule mapping are in `references/appendix.md`.

## When to use this skill, and when not to

Use it when a control is announced as "button" or "link" with no name, a form field has no label, ARIA attributes are rejected by a validator, or `aria-label` is used on containers.

| Request | Use instead |
|---|---|
| Choosing `<button>` or `<a>`, headings, landmarks | `semantic-html.md` |
| Label text, error text and `autocomplete` on forms | `html-forms.md` |
| Tab order, focus ring, click handlers on divs | `keyboard-focus.md` |
| Contrast of text and focus rings | `colour-contrast.md` |
| Announcing status messages and toasts | `aria-live-announcements.md` |
| Native `<dialog>` and popover markup | `dialog-and-popover.md` |
| Screen reader testing, voice control, switch devices | Out of scope. Needs a person with the device |

## Guardrails

1. **Propose, then change.** Report and plan first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
2. **Pages you own only.** `name_snapshot.js` opens a page in a browser. Do not run it on sites the user does not own. Never ask for cookies, tokens or passwords. For a page behind a login, ask for a static HTML copy with test data.
3. **Data handled.** `aria_check.py` reads the files you pass. `name_snapshot.js` prints roles and names, and names can hold personal data (a signed-in user's name in an account menu, for example). Report selectors and line numbers, not names, and never paste page text, form values or screenshots into prompts or reports.
4. **Nothing is invented.** Every rule carries a source and a date. Version-dependent behaviour carries **verify before relying on this**.
5. **Not a conformance statement.** A clean run means the checked markup is sound. It is not a WCAG conformance claim and not legal advice. A conformance claim needs a human who has used the page with a screen reader.
6. **Refuses to:** put `aria-label` on a `div` or `span` to make a name appear, override visible text with a different `aria-label`, add ARIA where a native element does the job, hide focusable content with `aria-hidden`, or report a fix as verified without the browser read.

## Inputs the agent must have before starting

Ask for every missing item. Do not assume.

| Input | Ask exactly |
|---|---|
| HTML | "Give me the rendered HTML, not the template source, for each page or component. Framework output is fine." |
| Page | "Which page may I open in a browser? Give a local file or a URL you own, with test data." |
| Language | "Is the site translated, by you or by the browser? It changes how icon buttons should be named." |
| Custom widgets | "Which components are not native elements: menus, tabs, comboboxes, switches, sliders?" |
| Tooling | "Is Node with Playwright installed? If not, may I install it in a temporary folder?" |
| Approval | "May I edit on a branch in batches and show each diff first?" |

## Workflow

### Step 1: Run the static checker

```
python3 scripts/aria_check.py page.html | head -40
```

Read only its output. Do not read the script. The first line holds the counts. Findings are sorted errors, warnings, info. `--strict` exits 1 on warnings.

| Code | Level | Meaning | Fix |
|---|---|---|---|
| `name-missing` | error | Button, link, `role=img` svg or named role with no text, label or alt | Step 3 |
| `field-unlabelled` | error | Input, select or textarea with no label element, `aria-label` or `aria-labelledby` | Step 3 |
| `label-title-only` | warn | Field with only `title` or `placeholder`, or a button named only by `title` (`name-title-only`) | Step 3 |
| `img-alt-missing` | error | `<img>` with no `alt` attribute | Step 3 |
| `name-on-generic` | error | `aria-label` or `aria-labelledby` on `div`, `span` or another generic element | Step 4 |
| `label-in-name` | warn | `aria-label` does not contain the visible text | Step 3 |
| `idref-missing` | error | `aria-labelledby` or `aria-describedby` points to an absent id | Step 5 |
| `labelledby-chained` | warn | Target of `aria-labelledby` itself uses `aria-labelledby` | Step 5 |
| `role-redundant` | warn | Role repeats the element's native role. `list` on `ul` is info | Step 4 |
| `role-invalid`, `role-abstract` | error | Role is not in WAI-ARIA, or is abstract | Step 4 |
| `role-required-attr` | error | A role without an attribute it needs | Step 4 |
| `hidden-focusable` | error | `aria-hidden="true"` on or around focusable content | Step 4 |

The checker reads markup. It does not run scripts, so names set in JavaScript are invisible to it, and it cannot compute names from CSS content.

### Step 2: Read the browser's computed names

```
node scripts/name_snapshot.js page.html
node scripts/name_snapshot.js page.html --all
```

The first run prints one line: `interactive-named=N empty-name=N aria-hidden-focusable=N`, then one line per empty name. Add `--all` only when you need to confirm what a fix produced. The source of each name is shown after `via=`.

| Output | Meaning | Action |
|---|---|---|
| `empty-name` line | The browser computed no name | Step 3 |
| `aria-hidden-focusable>0` | Focusable content sits under `aria-hidden` | Step 4 |
| `via=placeholder` or `via=title` | A fallback gave the name | Step 3, label-title-only |
| `via=contents` on a control you labelled | `aria-label` or `aria-labelledby` was not applied | Check for the target id and a typo |
| Checker is clean and the walk shows an empty name | Name comes from script or an unnamed custom element | Inspect the component |

Playwright must resolve through `require('playwright')`. Chromium only: Safari and Firefox compute names in their own way and need a hand check.

### Step 3: Fix names

Use the first row that applies. Order follows the browser's own computation order (`references/names-and-roles.md`, section 1).

| Situation | Fix |
|---|---|
| Control has visible text | Leave it. Add nothing |
| Field | A visible `<label for>`. If the visible text is a heading elsewhere, `aria-labelledby`. `aria-label` only when no visible label can exist (a search box beside its button) |
| Icon-only button | `aria-label` on the button and `aria-hidden="true" focusable="false"` on the svg. If browsers translate the site, use visually hidden text instead: `aria-label` is harder to translate (TPGi) |
| Image link | `alt` on the image that says where the link goes |
| Repeated "Read more" links | Give each a distinct name: `aria-labelledby="own-id heading-id"` |
| Group of related fields | `<fieldset>` with `<legend>` |
| `label-in-name` | Make the accessible name contain the visible text. Speech-input users say what they see (TPGi, axe `label-content-name-mismatch` for SC 2.5.3). Simplest fix: delete the `aria-label` |
| `title` or `placeholder` only | Add a real label. Neither is a dependable name |
| Decorative image | `alt=""` |

The name is what screen reader users hear and what speech-input users say to activate the control (TPGi). Write it as the action or destination, for example "Open menu".

### Step 4: Fix roles and ARIA attributes

| Situation | Fix |
|---|---|
| `name-on-generic` | Remove the `aria-label`. If a landmark is wanted, change the element: `<nav aria-label>`, `<section aria-labelledby>`. If a group is wanted, `role="group"` is valid and the name then applies |
| `role-redundant` | Delete the role. Exception: `role="list"` on a `ul` styled with `list-style: none`, kept for Safari. State it in the report and mark verify |
| `role-invalid`, `role-abstract` | Delete it, then ask what the element does and use the native element (`<select>` and `combobox`, not `select`) |
| Fake control (`span role="button"`) | Replace with `<button type="button">`. Key handling belongs to `keyboard-focus.md` |
| `role-required-attr` | Add the attribute and wire it to state in script. A static `aria-checked="false"` that never changes is worse than none |
| `hidden-focusable` | Use `inert` or `hidden` on the container, or remove the focusable item from the tab order. Never leave focusable content hidden from assistive technology |
| ARIA does what a native element does | Use the native element. A role promises keyboard behaviour that ARIA does not add (W3C APG) |

Required attributes covered by the checker are listed in `references/names-and-roles.md`, section 4. For roles outside it, do not guess: the ARIA 1.2 text decides.

### Step 5: References between elements

| Rule | Source |
|---|---|
| `aria-labelledby` can point at hidden elements and joins several ids with spaces | W3C APG |
| It is not followed through a chain: a target that also has `aria-labelledby` is read for its own text only | W3C APG |
| Ids must match exactly and cannot contain spaces | TPGi |
| It overrides `aria-label`, native labels and content. Do not set both | W3C APG |
| `aria-describedby` is announced after name and role and some screen readers skip it by default. Never the only home for an error or instruction | W3C APG |

### Step 6: Verify and report

1. Re-run `aria_check.py`. Errors must be 0. Each warning is fixed or listed with a reason.
2. Re-run `name_snapshot.js`. `empty-name` and `aria-hidden-focusable` must be 0. Run with `--all` once and read the names aloud against the visible page.
3. Check each changed icon button and each repeated link by looking at its name in the `--all` list.
4. State what is untested: screen readers, voice control, Safari and Firefox name computation, scripted names.
5. Use the report template. Do not echo unchanged markup back.

## Worked example

An invented page of 17 lines (`demo-names.html`): a header with a `div aria-label`, an icon-only button, a `button role="button"`, an image link with no `alt`, a `span role="button"`, an email input with a placeholder, a `div role="checkbox"`, an `aria-hidden` block holding a link, a button with a dead `aria-labelledby`, a button whose `aria-label` ignores its visible text, a link with `role="link"`, a `ul role="list"`, and `role="widget"`.

```
7 errors, 4 warnings, 1 info
demo-names.html:2  error  name-on-generic  aria-label/aria-labelledby on <div> with no naming role ...
demo-names.html:3  error  name-missing  <button> has no text, aria-label, aria-labelledby or alt
demo-names.html:5  error  img-alt-missing  <img> has no alt attribute (use alt="" if decorative)
demo-names.html:10  error  role-required-attr  role="checkbox" needs aria-checked
demo-names.html:11  error  hidden-focusable  aria-hidden="true" on or around focusable content ...
demo-names.html:12  error  idref-missing  aria-labelledby points to id "nope" which does not exist
demo-names.html:16  error  role-abstract  role="widget" is abstract and must not be used
demo-names.html:4  warn  role-redundant  <button> already has role button
demo-names.html:9  warn  label-title-only  <input type=email> has no label element ...
demo-names.html:13  warn  label-in-name  aria-label "Checkout" does not contain visible text "Pay now" ...
demo-names.html:14  warn  role-redundant  <a> already has role link
demo-names.html:15  info  role-redundant  <ul> already has role list (list-style:none can justify it ...)
```

(Messages shortened here.) The browser read prints `interactive-named=9 empty-name=1 aria-hidden-focusable=1` and `empty-name  button`. Two results are not in the checker output. The email input is named "Email" by the browser, with `via=placeholder`, so a screen reader hears a name and the defect is invisible to an audit that only looks for empty names. The dead `aria-labelledby="nope"` falls back to the button's content, so "Pay" is announced and the broken reference does nothing.

One batch: `div` becomes `<nav aria-label="Main menu">`, the icon button gets `aria-label="Open menu"` and an `aria-hidden` svg, the redundant roles go, the image gets `alt="Basket"`, the `span` becomes a `<button type="button">`, the email input and checkbox get labels, the `aria-hidden` wrapper loses its attribute, and the two mismatched or dead `aria-label` and `aria-labelledby` attributes are deleted. The re-run prints `0 errors, 0 warnings, 1 info` (the kept `role="list"`) and `interactive-named=11 empty-name=0 aria-hidden-focusable=0`.

## Templates

### Report

```
ARIA names and roles report: <page or repo>, <date>
Files: <list>. Browser read: <Chromium version>.
aria_check before: N errors, N warnings. After: N errors, N warnings.
name_snapshot before/after: empty-name N/N, aria-hidden-focusable N/N.
Changes: <file:line  issue  fix>   (one line each)
Left as is: <item  reason>
Untested: screen readers, voice control, Safari and Firefox, scripted names (edit to match)
Not a WCAG conformance claim. A human must test with a screen reader.
```

### Starting prompt

```
Use the aria-names-roles skill. Rendered HTML: <paths>. Page I own for the browser read: <path or URL>.
Custom widgets: <list>. Site translated by browsers: <yes|no>. Node and Playwright: <yes|no>.
Edit on a branch in batches of one component and show each diff first.
```

## Failure modes and anti-patterns

| Mistake | How to spot it | Do instead |
|---|---|---|
| `aria-label` on `div` or `span` to name a region | `name-on-generic`. JAWS 2026 and NVDA 2026.1.1 read content and skip the label in the May 2026 test | Native landmark element, or `role="group"` |
| ARIA added to fix what native HTML does | `role-redundant`, `span role="button"` | Native element (step 4) |
| `aria-label` that disagrees with visible text | `label-in-name` | Name contains the visible text (step 3) |
| `placeholder` or `title` treated as the label | `label-title-only`. The browser still reports a name via the placeholder | A real `<label>` |
| `aria-hidden` on something focusable | `hidden-focusable`, `aria-hidden-focusable>0` | `inert` or remove from tab order (step 4) |

## Quality bar

Run every item before handing over.

1. `aria_check.py` errors are 0, and every warning and info line is fixed or listed with a reason.
2. `name_snapshot.js` ran on the changed page, with `empty-name=0` and `aria-hidden-focusable=0`.
3. No `aria-label` or `aria-labelledby` remains on a generic element.
4. Every `aria-label` contains the visible text of its control.
5. Every `aria-labelledby` and `aria-describedby` id exists.
6. No native element carries a role it already has, except a noted `role="list"`.
7. Every custom role has the attributes it needs and they change with state.
8. The report lists what was not tested and states it is not a WCAG conformance claim.

## Cost

Figures are estimates from this skill's own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. Rival figures are estimates from the visible text of pages I read.

| Item | Tokens | Status | Read condition |
|---|---|---|---|
| A. Description (always loaded) | 100 | Estimate | Every session with the skill installed |
| B. `SKILL.md` body | 4,470 | Estimate | When the skill fires |
| C. `references/names-and-roles.md` | 1,134 | Estimate | Steps 3 to 4 when a recipe, naming rule or required attribute is needed |
| C. `references/appendix.md` | 742 | Estimate | Only for figures, axe rule ids or the rival comparison |
| Script output (typical run) | 450 | Estimate | Checker and snapshot output measured on the worked example |
| D. Typical run (A + B + script output) | 5,020 | Estimate | Common path, no references |
| D. Worst case (A + B + both references + 40 lines of output) | 7,850 | Estimate | Large input, every branch |
| E. Cheaper path | 4,940 | Estimate | Run the checker, fix flagged lines, skip the browser read detail and the references |

Closest rivals read about 270 to 1,000 tokens (estimates from visible text), but none ships a checker or reads computed names. This skill costs more to load, and in return it measures names instead of asking the agent to judge them.

## Handoffs

`keyboard-focus.md` covers focus and key handling. `semantic-html.md` and `html-forms.md` cover native elements and labels. `aria-live-announcements.md` covers status messages. `colour-contrast.md` covers contrast.

## Appendix

Lookup tables are in `references/names-and-roles.md`. Measurements and axe rule mapping are in `references/appendix.md`. Read them only when a step points there.

## Sources

Last verified 30 October 2026 for every source.

- Anthropic, Skill authoring best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- W3C, WAI-ARIA Authoring Practices, Names and descriptions: https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/
- W3C, WAI-ARIA Authoring Practices, Read me first: https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/
- W3C, ARIA in HTML: https://www.w3.org/TR/html-aria/
- Deque, axe-core rule descriptions (4.13): https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md
- WebAIM, The WebAIM Million 2026: https://webaim.org/projects/million/
- Manuel Matuzovic, Don't put aria-label on generic elements like divs (2026): https://matuzo.at/blog/2026/aria-label-generic-elements
- TPGi, Short note on aria-label, aria-labelledby, and aria-describedby: https://vispero.com/resources/short-note-on-aria-label-aria-labelledby-and-aria-describedby/
- Freedom Scientific, standards-support issue 759: https://github.com/FreedomScientific/standards-support/issues/759
- thedaviddias, aria-labels. Rival implementation: https://www.typingmind.com/skills/thedaviddias-aria-labels
- ibelick, fixing-accessibility. Rival implementation: https://claudeskills.info/skills/ibelick/ui-skills/fixing-accessibility/
- spencerpauly, accessibility-auditing. Rival implementation: https://claudeskills.info/skills/spencerpauly/awesome-cursor-skills/accessibility-auditing/

---
Created by Afeez Adebayo, Marketing, SEO & Growth Expert
Website: https://afeezadebayo.com
LinkedIn: https://www.linkedin.com/in/afeez-adebayo/
X: https://x.com/za_icon
Source: https://afeezadebayo.com/skills-md/frontend-development/accessibility/aria-names-roles/

Licence: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). You may copy, share and adapt this skill, including for commercial use, as long as you credit Afeez Adebayo and link to the source URL above. Keep this notice in any copy or adapted version.
