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.
By Afeez Adebayo, Marketing, SEO & Growth Expert with 10+ years of experience.
Created 30 October 2026. Updated 30 October 2026. Free to use under CC BY 4.0.
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
- 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.
- Pages you own only.
name_snapshot.jsopens 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. - Data handled.
aria_check.pyreads the files you pass.name_snapshot.jsprints 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. - Nothing is invented. Every rule carries a source and a date. Version-dependent behaviour carries verify before relying on this.
- 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.
- Refuses to: put
aria-labelon adivorspanto make a name appear, override visible text with a differentaria-label, add ARIA where a native element does the job, hide focusable content witharia-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?" |
Raw SKILL.md file
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 -40Read 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 --allThe 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
- Re-run
aria_check.py. Errors must be 0. Each warning is fixed or listed with a reason. - Re-run
name_snapshot.js.empty-nameandaria-hidden-focusablemust be 0. Run with--allonce and read the names aloud against the visible page. - Check each changed icon button and each repeated link by looking at its name in the
--alllist. - State what is untested: screen readers, voice control, Safari and Firefox name computation, scripted names.
- 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.
aria_check.pyerrors are 0, and every warning and info line is fixed or listed with a reason.name_snapshot.jsran on the changed page, withempty-name=0andaria-hidden-focusable=0.- No
aria-labeloraria-labelledbyremains on a generic element. - Every
aria-labelcontains the visible text of its control. - Every
aria-labelledbyandaria-describedbyid exists. - No native element carries a role it already has, except a noted
role="list". - Every custom role has the attributes it needs and they change with state.
- 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/
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, andscripts/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.
| Item | Tokens | Status | Read when |
|---|---|---|---|
| Description | 100 | Estimate | Every session with the skill installed |
SKILL.md body | 4,470 | Estimate | When the skill fires |
references/names-and-roles.md | 1,134 | Estimate | Steps 3 and 4, for a recipe, naming rule or required attribute |
references/appendix.md | 742 | Estimate | Only for figures, axe rule ids or the rival comparison |
| Typical run | 5,020 | Estimate | Description, body and script output |
| Worst case | 7,850 | Estimate | Both references and 40 lines of output |
| Cheaper path | 4,940 | Estimate | Run 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
divand 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 abutton. - 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
inertor removes the item from the tab order.
Related skills
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
What is an accessible name?
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.
Can I put aria-label on a div?
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.
Should I use aria-label or aria-labelledby?
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.
Why does my button read as just "button"?
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.
Does adding ARIA make a page more accessible?
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.
Does this replace axe or screen reader testing?
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.
How many tokens does the ARIA Names and Roles skill use?
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.
