Accessibility skill 3 of 12

ARIA Names and Roles SKILL.md: Fix Unnamed Controls and Misused Roles

The ARIA Names and Roles skill makes Claude, Cursor or another AI agent find buttons, links and fields that a screen reader cannot name, and ARIA roles that are redundant or invalid. A static checker reads your HTML, then the browser’s own computed names confirm every fix, so nothing is called fixed because the code looks right.

aria-names-roles/SKILL.md, version 1.0.0, 2,966 words, plus scripts/aria_check.py and scripts/name_snapshot.js.

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.

RequestUse instead
Choosing <button> or <a>, headings, landmarkssemantic-html.md
Label text, error text and autocomplete on formshtml-forms.md
Tab order, focus ring, click handlers on divskeyboard-focus.md
Contrast of text and focus ringscolour-contrast.md
Announcing status messages and toastsaria-live-announcements.md
Native <dialog> and popover markupdialog-and-popover.md
Screen reader testing, voice control, switch devicesOut 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.

InputAsk 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?"

Raw SKILL.md file

.md

This is the exact file agents read, including the credit header and licence notice. Save it as aria-names-roles/SKILL.md.

---
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.

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.

CodeLevelMeaningFix
name-missingerrorButton, link, role=img svg or named role with no text, label or altStep 3
field-unlabellederrorInput, select or textarea with no label element, aria-label or aria-labelledbyStep 3
label-title-onlywarnField with only title or placeholder, or a button named only by title (name-title-only)Step 3
img-alt-missingerror<img> with no alt attributeStep 3
name-on-genericerroraria-label or aria-labelledby on div, span or another generic elementStep 4
label-in-namewarnaria-label does not contain the visible textStep 3
idref-missingerroraria-labelledby or aria-describedby points to an absent idStep 5
labelledby-chainedwarnTarget of aria-labelledby itself uses aria-labelledbyStep 5
role-redundantwarnRole repeats the element's native role. list on ul is infoStep 4
role-invalid, role-abstracterrorRole is not in WAI-ARIA, or is abstractStep 4
role-required-attrerrorA role without an attribute it needsStep 4
hidden-focusableerroraria-hidden="true" on or around focusable contentStep 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=.

OutputMeaningAction
empty-name lineThe browser computed no nameStep 3
aria-hidden-focusable>0Focusable content sits under aria-hiddenStep 4
via=placeholder or via=titleA fallback gave the nameStep 3, label-title-only
via=contents on a control you labelledaria-label or aria-labelledby was not appliedCheck for the target id and a typo
Checker is clean and the walk shows an empty nameName comes from script or an unnamed custom elementInspect 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).

SituationFix
Control has visible textLeave it. Add nothing
FieldA 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 buttonaria-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 linkalt on the image that says where the link goes
Repeated "Read more" linksGive each a distinct name: aria-labelledby="own-id heading-id"
Group of related fields<fieldset> with <legend>
label-in-nameMake 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 onlyAdd a real label. Neither is a dependable name
Decorative imagealt=""

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

SituationFix
name-on-genericRemove 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-redundantDelete 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-abstractDelete 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-attrAdd the attribute and wire it to state in script. A static aria-checked="false" that never changes is worse than none
hidden-focusableUse 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 doesUse 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

RuleSource
aria-labelledby can point at hidden elements and joins several ids with spacesW3C APG
It is not followed through a chain: a target that also has aria-labelledby is read for its own text onlyW3C APG
Ids must match exactly and cannot contain spacesTPGi
It overrides aria-label, native labels and content. Do not set bothW3C 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 instructionW3C 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

MistakeHow to spot itDo instead
aria-label on div or span to name a regionname-on-generic. JAWS 2026 and NVDA 2026.1.1 read content and skip the label in the May 2026 testNative landmark element, or role="group"
ARIA added to fix what native HTML doesrole-redundant, span role="button"Native element (step 4)
aria-label that disagrees with visible textlabel-in-nameName contains the visible text (step 3)
placeholder or title treated as the labellabel-title-only. The browser still reports a name via the placeholderA real <label>
aria-hidden on something focusablehidden-focusable, aria-hidden-focusable>0inert 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.

ItemTokensStatusRead condition
A. Description (always loaded)100EstimateEvery session with the skill installed
B. SKILL.md body4,470EstimateWhen the skill fires
C. references/names-and-roles.md1,134EstimateSteps 3 to 4 when a recipe, naming rule or required attribute is needed
C. references/appendix.md742EstimateOnly for figures, axe rule ids or the rival comparison
Script output (typical run)450EstimateChecker and snapshot output measured on the worked example
D. Typical run (A + B + script output)5,020EstimateCommon path, no references
D. Worst case (A + B + both references + 40 lines of output)7,850EstimateLarge input, every branch
E. Cheaper path4,940EstimateRun 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.

What this skill does

WebAIM’s 2026 survey of one million home pages (tested February 2026) found detectable WCAG 2 failures on 95.9% of them. Missing form labels appeared on 51%, empty links on 46.3% and empty buttons on 30.6%. Pages that used ARIA averaged 59.1 detected errors against 42 for pages without it. That is a correlation, not proof of cause, and it is why the skill treats native HTML as the first fix.

The skill runs scripts/aria_check.py over your rendered HTML. It flags controls with no name, fields with no label, aria-label on div and span, aria-labelledby pointing at ids that do not exist, redundant, invalid and abstract roles, roles missing a required attribute, aria-hidden over focusable content, and labels that do not contain the visible text. Then scripts/name_snapshot.js opens a page you own in Chromium and prints the role and name the browser computed for every interactive element. Fixes are proposed first and edited in batches you approve.

Who it is for

Use it if you are a frontend developer, a theme builder or an accessibility tester, or if you review interfaces an AI tool generated. It fits when axe or Lighthouse reports button-name, label or aria-allowed-role failures, when a control is announced as just “button”, or when a design system names containers with aria-label. It does not cover contrast, focus order, live regions or screen reader testing.

What is inside

The file is 2,966 words (version 1.0.0) with two scripts and two reference files. It holds:

  • Six guardrails, including editing only on a branch after approval, running the browser read only on pages you own and never claiming WCAG conformance.
  • Six inputs the agent must have first, with the exact question for each.
  • A workflow of six steps, with tables that map each checker code to a fix.
  • Two tools: scripts/aria_check.py, a standard-library checker with fourteen problem codes, and scripts/name_snapshot.js, a Node and Playwright read of the names the browser computed.
  • A worked example with real output: 7 errors and 4 warnings before, 0 errors and 0 warnings after.
  • A report template, a starting prompt, five failure modes and an eight-item quality bar.
  • Two reference files, loaded only when a step points to them: lookup tables for names and roles, and an appendix with measurements and axe rule mapping.
ItemTokensStatusRead when
Description100EstimateEvery session with the skill installed
SKILL.md body4,470EstimateWhen the skill fires
references/names-and-roles.md1,134EstimateSteps 3 and 4, for a recipe, naming rule or required attribute
references/appendix.md742EstimateOnly for figures, axe rule ids or the rival comparison
Typical run5,020EstimateDescription, body and script output
Worst case7,850EstimateBoth references and 40 lines of output
Cheaper path4,940EstimateRun the checker, fix flagged lines, skip the references

The figures come from the skill’s own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. The three rivals I read run about 270 to 1,000 tokens by the same method, and none ships a checker.

How to install and use it in Claude, Cursor and other agents

Claude. Keep the folder as aria-names-roles/ with SKILL.md, scripts/ and references/ inside, and zip it. In Claude, open Customize, then Skills, then Upload a skill. In Claude Code, put the folder in ~/.claude/skills/ or .claude/skills/ in a project.

Cursor and other agents. Put the folder where your tool reads skills, or paste the body into its rules and keep the scripts beside your code. The browser read needs Node and Playwright. Then send the starting prompt from the Templates section.

Before you install. Read the scripts first. The checker reads files and writes nothing. The browser read opens a page you own and prints roles and names for interactive elements. The skill tells the agent to edit only on a branch in batches you approve. Install skills only from sources you trust.

A worked example

An invented 17-line page has a div with aria-label, an icon-only button, an image link with no alt, a span role="button", an email input that has only a placeholder, a button with a dead aria-labelledby, a button whose aria-label ignores its visible text, and role="widget". The checker reports 7 errors, 4 warnings and 1 info line, and the browser read finds one button with no name and one focusable link under aria-hidden.

Two findings never reach the checker output. Chromium names the email input “Email” from its placeholder, so a screen reader hears a name and an audit that only looks for empty names passes it. The dead aria-labelledby falls back to the button text, so the broken reference does nothing. One batch of fixes brings the checker to 0 errors and 0 warnings and the browser read to 0 empty names.

Common mistakes

The skill is built around five ways this goes wrong:

  • Naming a container with aria-label. In a May 2026 test JAWS 2026 and NVDA 2026.1.1 read the content of a labelled div and skipped the label. The skill moves the name to a native landmark.
  • Adding ARIA where HTML already does the job. A span role="button" promises keyboard behaviour that ARIA does not supply. The skill swaps in a button.
  • An aria-label that disagrees with visible text. Speech-input users say what they see. The skill requires the name to contain the visible text.
  • Treating a placeholder as the label. The browser still reports a name, which hides the defect. The skill asks for a real label.
  • Hiding focusable content with aria-hidden. Keyboard users can reach what screen reader users cannot. The skill uses inert or removes the item from the tab order.

Keyboard Focus covers tab order, focus rings and click handlers on divs. HTML Forms covers label text, hints and error messages. Semantic HTML covers the native buttons, links and landmarks that make many ARIA fixes unnecessary. ARIA Live Announcements covers status messages and toasts.

Frequently asked questions

It is the text a screen reader announces for a control. The browser takes it from aria-labelledby first, then aria-label, then a native label or alt text, then the control’s own content, with title and placeholder as fallbacks, according to the W3C Authoring Practices. The skill reads the computed name in Chromium so you see what was actually given.

Not to name it. ARIA in HTML says naming is prohibited on generic elements such as div and span. In Manuel Matuzovic’s May 2026 test, JAWS 2026 and NVDA 2026.1.1 ignored the label on a div with content and read the content. The skill moves the name to a native element such as nav or section.

Use a visible label first. Where ARIA is needed, aria-labelledby points at visible text, so the name stays in step when that text changes, and it overrides aria-label. TPGi notes aria-label is not shown on screen and is harder to translate. The skill uses aria-label only where no visible label can exist.

Because it has no name. An icon-only button with no aria-label, hidden text or title has nothing to announce. The checker flags it as name-missing, and the browser read lists it as an empty name. The fix is an aria-label on the button and aria-hidden on the icon, or visible text.

Not by itself. WebAIM found pages with ARIA averaged 59.1 detected errors against 42 without in its 2026 survey, a correlation and not proof of cause. The W3C says a role is a promise of behaviour. The skill uses native HTML first and ARIA only where HTML has no equivalent.

No. It covers the names and roles that code can measure and lists what it did not test, including screen readers, voice control and Safari. Its checker codes map to axe rules such as button-name, label and aria-allowed-role. A clean run is not a WCAG conformance claim, and a human should try the page.

The description is about 100 tokens and loads in every session. The SKILL.md body is about 4,470 tokens when the skill fires. A typical run is about 5,020 tokens with script output, the cheaper path is about 4,940, and the worst case with both references is about 7,850. All figures are estimates, not from a model tokeniser.

Changelog

  • 1.0.0, 30 October 2026: first release.

Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.