A screen reader user tabs into a text field and hears “edit text, blank”. That’s all they get. They don’t know whether it wants a name, an email, or a promo code. A sighted user sees “Email” printed above the box, but nothing in the markup connects that word to the input, so the screen reader can’t say it.
The thing missing is the field’s accessible name: the short text assistive technology announces for a control. For form fields, the <label> element is how you provide it, and getting it right is one of the most common accessibility fixes in real code reviews.
Two ways to associate a label
The explicit way uses for on the label and a matching id on the control:
<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email">The implicit way wraps the control in the label:
<label>
Email address
<input name="email" type="email" autocomplete="email">
</label>Both give the input the name “Email address”. The screen reader now says “Email address, edit text”. The explicit form is the safer default: it works when the label and input live in different layout containers (a two-column grid, for example), and some older assistive technology combinations handled wrapped labels less reliably. You can also combine the two.
Getting the association right brings a second benefit that sighted mouse and touch users notice: clicking the label focuses the input. For checkboxes and radio buttons, clicking the label toggles them, which turns a 16px square target into the whole line of text. On a phone that’s the difference between hitting the option and missing it.
Common ways it silently breaks:
- A typo or duplicate
id.for="emial"points at nothing; two inputs withid="email"(from rendering a component twice) make the second label point to the first input. Generate ids per instance in component frameworks (React’suseId, for example). forpointing at a wrapper.formust reference the form control itself, not a div around it.- The label is a
<span>or<p>. Text near an input isn’t a label. Nothing connects them.
Why the placeholder isn’t a label
A design that shows only placeholder="Email" inside the box looks clean and causes several problems at once:
- The text vanishes as soon as the user types, so anyone who loses their place (or has short-term memory difficulties) can’t check what the field was.
- Default placeholder grey is typically below the 4.5:1 contrast ratio WCAG asks for text.
- Browsers do use the placeholder as a fallback name if nothing else exists, but support across browser and screen reader pairs is inconsistent, and it’s announced as a hint in some and the name in others.
- Autofilled fields hide the placeholder, so after the browser fills in a form, the user sees values with no labels at all.
If the visual design insists on an in-field look, use a floating label: a real <label> positioned over the input that moves up when the field has focus or a value. The markup stays a proper label.
Hints, errors and descriptions
The name should be short. Extra guidance goes in a description, connected with aria-describedby:
<label for="password">Password</label>
<input
id="password"
name="password"
type="password"
autocomplete="new-password"
aria-describedby="password-hint password-error"
aria-invalid="true"
>
<p id="password-hint">At least 12 characters.</p>
<p id="password-error">Your password is only 8 characters long.</p>On focus, a screen reader reads the name, the role, the state and then the descriptions: roughly “Password, secure edit text, invalid entry. At least 12 characters. Your password is only 8 characters long.” The order of ids in aria-describedby is the order they’re read. aria-invalid="true" adds the “invalid” state; set it only after validation fails, not on page load.
Make the error text specific. “Invalid input” tells the user nothing; “Your password is only 8 characters long” tells them what to change. When the error appears dynamically, it won’t be announced unless focus moves to the field or the message is in a live region, so a common approach is to move focus to the first invalid field on submit, which reads its name and description.
Questions with several answers: fieldset and legend
A set of radio buttons has two levels of naming. Each option needs its own label, and the group needs a name for the question:
<fieldset>
<legend>Preferred contact method</legend>
<input type="radio" id="contact-email" name="contact" value="email">
<label for="contact-email">Email</label>
<input type="radio" id="contact-phone" name="contact" value="phone">
<label for="contact-phone">Phone</label>
</fieldset>When the user tabs into the group, the screen reader announces the legend and the option together: “Preferred contact method, grouping. Email, radio button, not checked, 1 of 2.” Without the fieldset, they’d hear “Email, radio button” with no idea what question it answers. The same applies to a group of related checkboxes and to multi-part inputs, such as a date split into day, month and year fields.
The default rendering draws a grooved border around the fieldset with the legend sitting in the top edge. Most designs reset that with border: 0; padding: 0; margin: 0; and style the legend like any other heading text; the semantics remain.
When there’s no visible text
Some controls genuinely have no visible label: a search input with a magnifying-glass button next to it, for example. You still need a name. Options, in order of preference:
- A visible label anyway. “Search” next to the box helps everyone.
- A label hidden visually but kept for assistive technology, with a
.visually-hiddenclass (the clip-path or clip pattern, notdisplay: none, which removes it from the accessibility tree too). aria-label="Search articles"directly on the input.aria-labelledbypointing at existing visible text, such as a heading, when that text already describes the field.
aria-labelledby wins over everything else, then aria-label, then the <label> element, then title and placeholder. Knowing that precedence explains bugs where a correct <label> seems to be ignored: someone added an aria-label that overrides it, and now the announced name differs from what’s on screen. Speech-input users say the visible text (“click Email address”), so the accessible name should contain the visible label text (WCAG 2.5.3, Label in Name).
Checking your forms
Chrome and Firefox DevTools show each element’s computed accessible name in the Accessibility pane. Select an input and confirm the name is what a user would expect to hear. Then click every label and confirm focus lands in the right field; this catches broken for/id pairs in seconds. Finally, tab through the form with a screen reader running and listen for any field announced only by its role.
Interview practice
A newsletter sign-up has a single input with placeholder="Your email" and a submit button showing a paper-plane icon. An audit says both controls lack accessible names. Fix them with minimal visual change.
Answer
Give the input a real label. If the design can’t show one, add <label for="newsletter-email" class="visually-hidden">Email address</label> and id="newsletter-email" on the input, and keep the placeholder as an example (you@example.com) rather than as the name. Add type="email" and autocomplete="email" so phones show the right keyboard and browsers can autofill. For the icon button, add text: either visually hidden text inside the button (“Subscribe”) or aria-label="Subscribe", and mark the SVG aria-hidden="true" so it isn’t announced as an unlabelled image. Then check the computed names in DevTools: “Email address, edit text” and “Subscribe, button”. The hidden label is a compromise: it names the field but gives sighted users no lasting reminder and no larger click target, so a visible label is still the better outcome if the design allows it.