Topic 7.5
Accessible Names and Forms
In one line
Every control has an accessible name that the browser computes in a fixed order (aria-labelledby, aria-label, the native label, the content, then title or placeholder); accessible forms pair that with visible labels, required and invalid states, and error messages linked by aria-describedby.
Think of it like this
Labels on a spice rack. Every jar needs a label, or you're tasting each one to find the cumin. If a jar has a printed label and a sticky note and handwriting on the lid, you need a rule for which one counts: the sticky note on top wins. Browsers have exactly such a rule for naming every button, link and field.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Accessible name
- The short text that identifies an element to assistive tech, like "Email" or "Subscribe".
- Accessible description
- Extra text read after the name and role, such as a hint or an error.
- accname
- The W3C spec with the exact rules browsers follow to compute names and descriptions.
- Name from content
- Getting the name from the text inside the element. Buttons, links and headings do this; divs don't.
- Placeholder
- Grey example text shown inside an empty field. It disappears when you type and is not a label.
- fieldset and legend
- A box that groups related fields, and its caption, which names the group.
- aria-invalid
- Says a field's value failed validation. Set it to
"true"only after checking. - Error summary
- A box at the top of a form listing every error, each linked to its field.
- Label in Name
- WCAG rule 2.5.3: the accessible name must include the text you can see on the control.
Step by step
01Follow the name order
For any element, the browser walks this list and stops at the first non-empty answer. That's why an aria-label silently replaces the visible text of a button, and why aria-labelledby beats everything.
02Label every field visibly
Explicit labels (for matching id) work everywhere and let you put the label anywhere. Wrapping labels need no ids. Both make the label text clickable.
<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email">
<label>
<input type="checkbox" name="news"> Send me the newsletter
</label>03Name groups with fieldset and legend
The legend becomes the group's name and is announced when focus enters the group.
<fieldset>
<legend>Delivery speed</legend>
<label><input type="radio" name="speed" value="std" checked> Standard</label>
<label><input type="radio" name="speed" value="exp"> Express</label>
</fieldset>04Add hints and mark required fields
A hint goes in its own element, linked with aria-describedby. required is announced and enforced. If you style required fields with an asterisk, explain it once at the top.
<p>Fields marked * are required.</p>
<label for="pw">Password *</label>
<input id="pw" name="pw" type="password" required
aria-describedby="pw-hint" autocomplete="new-password">
<p id="pw-hint">At least 12 characters.</p>05Show errors the accessible way
After the user submits, mark the field invalid, put the message in text next to it, link the message, and move focus. aria-describedby can list several ids, so the hint and the error are both read.
<label for="pw">Password *</label>
<input id="pw" name="pw" type="password" required
aria-invalid="true" aria-describedby="pw-error pw-hint">
<p id="pw-error" class="error">Error: Password must be at least 12 characters. You entered 7.</p>
<p id="pw-hint">At least 12 characters.</p>06Hear the result
When focus moves to the field, a screen reader says something like: "Password star, edit text, required, invalid entry, Error: Password must be at least 12 characters. You entered 7. At least 12 characters." Every part came from the HTML you wrote.
Try it yourself
- 1
Predict the winner
In the first example, add
aria-label="Your email"to inputi2. Predict its name before running. (The aria-label wins over the label. "Your email" still contains the visible word "Email", so it passes Label in Name, which ignores letter case; "Contact address" would not.) - 2
Name the empty button
Give button
b3a name two different ways: firstaria-label="Settings", then instead put<span class="visually-hidden">Settings</span>inside it. Run after each change. - 3
Check the real tree
Right-click the email field in the second example's preview → Inspect → open the Accessibility pane. Find the Name ("Email *"), the Description (the error text after a failed submit) and the
invalidstate.
Code & diagrams
This is a teaching version of the real algorithm. Check the real names in DevTools: Elements → Accessibility pane → Computed Properties → Name, which also shows where each name came from. Note b2: the visible "Save" is still inside the name "Save draft", so it passes Label in Name.
Preview
Console
Nothing logged yet.Expected console output
b1 -> Save (content)
b2 -> Save draft (aria-label)
i1 -> Billing address
i2 -> Email (label)
i3 -> Search (placeholder fallback)
l1 -> Home (content)
b3 -> NO NAME`novalidate` turns off the browser's own bubbles so we control the messages; the `validity` object still does the checking. The message starts with "Error:" so it isn't conveyed by red colour alone.
Preview
Console
Nothing logged yet.Expected console output
invalid: true | focused: true | message: Error: Enter your email address.
invalid: true | focused: true | message: Error: Enter an email like name@example.com.
invalid: false | focused: true | message: (none)Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use a placeholder instead of a label
Replace <label for="email">Email</label><input id="email"> with <input placeholder="Email">.
Break #2
Name a button differently from its text
<button aria-label="Submit the form">Send</button>
Break #3
Set aria-invalid before the user has typed
Ship the form with aria-invalid="true" on every empty required field.
Myth vs fact
Myth
A placeholder is a label.
Fact
It's a hint that disappears. It counts only as a weak fallback name, and WCAG and every design system recommend a visible label.
Myth
aria-label adds to the visible text.
Fact
It replaces it as the name. Screen readers read the aria-label, not the text inside.
Myth
Red borders are enough to show an error.
Fact
Colour alone fails WCAG 1.4.1 and is invisible to screen readers. Add text, and link it with aria-describedby.
Myth
The title attribute is a good way to label a field.
Fact
It is the last fallback, appears only on mouse hover, and isn't shown on touch devices. Use a label.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Test frameworks (Testing Library's
getByRole("button", { name: "Save" }), Playwright'sgetByRole) query the accessible name, so good names make tests sturdier than CSS selectors.dom-accessibility-apiimplements accname for jsdom-based tests. - ▸
aria-labelledbyandaria-describedbycan reference hidden elements (withhiddenordisplay: none) and still use their text. That's how a visually hidden label element can name a control. They can also reference the element itself:aria-labelledby="btn-id product-name"gives "Buy, Blue mug". - ▸
aria-errormessage(ARIA 1.1+) points to an error message and is only exposed whenaria-invalid="true". Screen reader support has improved but is still less consistent thanaria-describedby, so most production forms (including the GOV.UK Design System) usearia-describedbyfor errors. - ▸
CSS
:user-invalid(all major browsers since 2023) matches a field only after the user has interacted with it, which avoids showing errors on untouched fields. Pair it witharia-invalidset from script, because CSS state isn't exposed to assistive tech.
Remember this
- 1
The accessible name is the short text that identifies an element to assistive tech: what a screen reader says ("Email, edit text"), what voice control users say ("click Subscribe") and what automated tests search for (
getByRole("button", { name: "Subscribe" })). The accessible description is extra, optional information read after it. Both are computed by the browser following the W3C Accessible Name and Description Computation spec ("accname") plus the HTML Accessibility API Mappings. - 2
The order, simplified, first match wins: 1.
aria-labelledby: the text of the referenced elements, joined by spaces (it even works when they are hidden). 2.aria-label. 3. The native label from HTML: a<label>for form fields,altfor images,<legend>for a fieldset,<caption>for a table,<figcaption>for a figure, thevalueof<input type="submit">. 4. Name from content: for roles like button, link, heading, tab and cell, the text inside, including the alt of images inside. 5. A last-resort fallback:title, and for text fieldsplaceholder. If all are empty, the element has no name, which for an interactive element is a WCAG 4.1.2 failure. - 3
Name from content only applies to certain roles. Buttons, links, headings, menu items, options and table cells get their name from what's inside. A
div,sectionorformdoes not: putting text inside aregiondoesn't name it; you needaria-labelledbypointing at its heading. - 4
Forms need visible labels. A
<label for="id">(or a label wrapping the field) names the field, and clicking the label focuses or toggles it, which gives a bigger click target. Placeholder is not a label: it disappears as soon as you type, often has low contrast, and is only a fallback name. WCAG 2.5.3 Label in Name (A) says the accessible name must contain the visible label text, so voice users can say what they see: a button showing "Send" must not be named "Submit form". - 5
Groups need names too. Radio buttons and related checkboxes go in a
<fieldset>with a<legend>: a screen reader says "Delivery speed, grouping" and then "Express, radio button, 1 of 2". Without the legend, the user hears "Express, radio button" and has to guess the question. - 6
Required and invalid. The
requiredattribute is exposed as "required" to assistive tech and also triggers browser validation (Topic 4.7); show it visually too (the word "required", or an asterisk explained at the top of the form). After validation fails, setaria-invalid="true"on the field (not before the user has tried), write the error as text next to the field (not colour alone: WCAG 1.4.1), and link it witharia-describedby="email-error"so it's read with the field. On submit, move focus to the first invalid field or to an error summary that links to each field. WCAG 3.3.1 Error Identification (A) and 3.3.3 Error Suggestion (AA) ask you to say what's wrong and how to fix it.
Explain it without notes
Walk through how a browser computes the accessible name of a button or input.
Why isn't placeholder text a replacement for a label?
How should a form announce a validation error to a screen reader user?
What is WCAG 2.5.3 Label in Name, and how does aria-label commonly break it?
Practice
Write an accessible "Choose a plan" group with three radio options (Free, Pro, Team), with Free selected.
Write a phone field with a visible label, a hint "Include your country code", required, and an error message slot, all correctly linked. Show the HTML after a failed submit.
Fix this: <input type="search" placeholder="Search"><button><img src="search.svg"></button>.
Trade-offs
- ↔
Inline validation while typing gives fast feedback but can announce errors before the user has finished; validating on submit and on leaving a field is calmer and is what most accessible design systems do.
- ↔
An error summary plus inline messages is more work than inline messages alone, but on long forms it lets users see every problem at once and jump to each field.
Done when you can
I can list the accessible name order and predict which source wins.
I give every field a visible label and every radio group a legend.
I link hints and errors with
aria-describedby.I set
aria-invalidonly after validation and move focus to the problem.I keep the visible label inside the accessible name.
I can read the name, description and states in the DevTools Accessibility pane.