// guide
Form Labeling: Deep Dive
Form labeling is the single highest-impact accessibility decision in any form: get it right and screen reader users hear exactly what each input is for; get it wrong and the form is functionally unusable for them. This guide covers everything from the native <label> element to ARIA labeling attributes and the rules that prevent forms from breaking with screen readers.
// 01 · why labels matter more than anything
Why Labels Matter More Than Anything
Without a label, a screen reader announces an input as "edit blank" or simply "edit text". The user has no idea what to type. They can hear the field exists, they can focus it, they can even type into it — but they don't know what it's for.
Sighted users almost never notice missing labels because they read them from context: a heading nearby, the field's position in the layout, an icon, a placeholder, the surrounding paragraph. Assistive technology users have none of that context unless it's encoded in the markup. The label is the context.
Three WCAG criteria rest directly on getting labels right:
- 1.3.1 Info and Relationships — the label-input relationship must be programmatic, not just visual.
- 3.3.2 Labels or Instructions — every input must have a label or clear instructions.
- 4.1.2 Name, Role, Value — every interactive element must have an accessible name.
Every other form accessibility concern — error messaging, required fields, validation, autofill — depends on the label being correct first. If the label is wrong or missing, nothing else you add to the form helps.
// 02 · the native <label> element
The Native <label> Element
The native HTML <label> element is the most reliable, well-supported, and feature-rich way to label a form control. Reach for it first. Always.
There are two ways to associate a <label> with an input.
Explicit association (preferred)
The label has a for attribute pointing to the input's id. The two values must match exactly.
<label for="email">Email address</label>
<input type="email" id="email" name="email">
This is the most flexible form. The label can be anywhere in the DOM relative to the input — above, below, in a separate column, in a different parent — as long as the for and id match.
Implicit association (wrapping)
The input is nested inside the <label>. No for or id required.
<label>
Email address
<input type="email" name="email">
</label>
Works for simple cases. Less flexible — the label and input must share a parent. Some older assistive tech has bugs with implicit labels when extra wrapping markup (icons, hint text) sits between the label text and the input.
Why explicit usually wins
Explicit labels survive layout changes. You can move the label into a left-hand column, float it above, position it as a placeholder-style overlay, or restructure the form for mobile — and the association still works because for/id doesn't care about DOM nesting. Implicit labels lock the structure.
The click-target benefit
A correctly associated label is also a click target. Clicking the word "Email address" focuses the input — for text fields, places the cursor; for checkboxes and radios, toggles them. This benefits everyone: motor-impaired users, mobile users with small touch targets, and anyone who clicks slightly off the input. You get this for free with both forms of association. You don't get it with aria-label or aria-labelledby.
<label for> with matching id on the input. It's the most flexible, the best supported, and the easiest to maintain when designs change.
// 03 · which inputs need labels
Which Inputs Need Labels
Most form controls need a label. A few derive their accessible name from a different attribute. Here's the complete list.
| Element | Needs <label>? | Notes |
|---|---|---|
<input type="text"> |
Yes | Same for email, tel, url, number, password, search, date, time, color, file, range. |
<input type="checkbox"> |
Yes | Plus a group label via <fieldset>/<legend> when multiple related checkboxes appear together. |
<input type="radio"> |
Yes | Each radio needs its own label and the group needs a <fieldset>/<legend>. |
<textarea> |
Yes | Same rules as <input>. |
<select> |
Yes | Label the select itself. Don't try to label individual <option> elements — they're already self-describing. |
<meter> |
Yes | Even read-only widgets need a label so screen readers announce what the value represents. |
<progress> |
Yes | Same — the percentage means nothing without a label. |
<output> |
Yes | Often paired with aria-live for dynamic results. |
<input type="submit"> |
No | Accessible name comes from value. Default is "Submit". |
<input type="button"> |
No | Accessible name comes from value. |
<input type="reset"> |
No | Accessible name comes from value. Default is "Reset". Avoid using reset buttons in real forms. |
<input type="image"> |
No | Accessible name comes from alt. Treated as a button by screen readers. |
<input type="hidden"> |
No | Not exposed to assistive tech at all. |
<button> |
No | Accessible name comes from the text content (or aria-label for icon-only buttons). |
The pattern: anything that accepts user input needs a <label>. Anything that performs an action derives its name from its visible text or value/alt.
// 04 · grouping with <fieldset> and <legend>
Grouping with <fieldset> and <legend>
Sometimes a single label per input isn't enough. Radio buttons need a label per option and a label for the question they're answering. A set of address fields needs labels per field and a label for the address as a whole. That's where <fieldset> comes in.
A <fieldset> wraps a group of related fields. Its first child is a <legend> that names the group. Screen readers announce the legend along with each field's individual label, so the user always knows which group they're inside.
Radio button group
<fieldset>
<legend>Shipping speed</legend>
<input type="radio" id="ship-std" name="shipping" value="standard">
<label for="ship-std">Standard (5-7 days)</label>
<input type="radio" id="ship-exp" name="shipping" value="express">
<label for="ship-exp">Express (2-3 days)</label>
<input type="radio" id="ship-ovr" name="shipping" value="overnight">
<label for="ship-ovr">Overnight</label>
</fieldset>
A screen reader user navigating to the first radio hears: "Shipping speed, Standard 5 to 7 days, radio button, 1 of 3, not checked." The legend prefixes every field announcement in the group.
Checkbox group
Same structure when checkboxes share a question.
<fieldset>
<legend>Newsletter preferences</legend>
<input type="checkbox" id="news-prod" name="news" value="product">
<label for="news-prod">Product updates</label>
<input type="checkbox" id="news-blog" name="news" value="blog">
<label for="news-blog">New blog posts</label>
</fieldset>
Related field groups
Use <fieldset> when several inputs together answer one logical question — billing address, full name with first/middle/last, credit card number split across multiple inputs, date of birth as separate day/month/year fields.
<fieldset>
<legend>Shipping address</legend>
<label for="addr-line1">Street address</label>
<input id="addr-line1" name="line1" autocomplete="address-line1">
<label for="addr-city">City</label>
<input id="addr-city" name="city" autocomplete="address-level2">
<label for="addr-zip">Postal code</label>
<input id="addr-zip" name="zip" autocomplete="postal-code">
</fieldset>
Styling fieldsets
Browsers add a default border and padding to <fieldset>. Most designs strip it.
fieldset {
border: 0;
padding: 0;
margin: 0;
}
legend {
/* Style as a heading */
font-weight: 600;
font-size: 1.125rem;
margin-bottom: var(--space-md);
}
Stripping the border doesn't remove the semantic group — assistive tech still announces the legend. The grouping is in the markup, not the visual styling.
<fieldset> for genuinely related groups where the legend adds meaning to each child label.
// 05 · aria labeling: when and why
ARIA Labeling: When and Why
ARIA gives you three labeling attributes. They exist for situations where the native <label> element doesn't fit. Don't reach for them first.
| Attribute | What it does | When to use |
|---|---|---|
aria-labelledby |
References one or more existing element IDs whose text content becomes the input's accessible name. | When visible label text already exists on screen but isn't a <label> element — a heading, a paragraph, a card title. |
aria-label |
Provides a text string directly as the accessible name. Not visible on screen. | Last resort. When no visible text exists — icon-only buttons, magnifier-icon-only search fields, controls in dense toolbars. |
aria-describedby |
References element IDs whose text becomes additional description, read after the name. | Hint text, format examples, error messages. Not the primary name. |
Decision rules
- If you can use
<label>, use<label>. It's the most reliable and gives you the click-target behavior for free. - If visible text already exists and can't be moved into a
<label>, usearia-labelledby. Don't duplicate text — reference it. - If no visible text exists at all, use
aria-label. Only when a visible label genuinely can't be added (and ask first whether it should be). - For supplementary detail, use
aria-describedby. Hint text, character limits, format examples, error messages — anything that isn't the primary name.
aria-labelledby example
Imagine a card with a heading "Search products" and a search input below it. The heading is the natural label, but it's an <h2> not a <label>.
<h2 id="search-heading">Search products</h2>
<input type="search" aria-labelledby="search-heading">
The screen reader announces "Search products, search edit." No duplicate text on screen. Note: aria-labelledby can reference multiple IDs, joined with spaces — the result is concatenated in order.
aria-label example
A search input where the only visible adornment is a magnifier icon.
<input type="search" aria-label="Search articles">
<button type="submit" aria-label="Submit search">
<svg aria-hidden="true" ...><!-- magnifier --></svg>
</button>
This works, but consider whether a visible label would help everyone. Sighted users with cognitive disabilities, low literacy, or unfamiliarity with the icon also benefit from text.
aria-describedby example
A password field with format requirements.
<label for="pw">Password</label>
<input type="password" id="pw" aria-describedby="pw-hint">
<p id="pw-hint">At least 12 characters, including a number and a symbol.</p>
The screen reader announces "Password, edit, secure," then reads the hint. The label is still <label>; the description supplements it.
aria-label only exists in the accessibility tree. Sighted users never see it. Auto-translation tools sometimes skip it. Voice-control users can't say its text to activate the control unless the visible label includes that text. Always prefer a visible label.
// 06 · placeholder ≠ label
Placeholder ≠ Label
The single most common form labeling mistake: using placeholder as the only label. It's quick, it looks clean, it satisfies the visual designer who didn't want the form to feel cluttered. It's also broken.
Why placeholders fail as labels
- They disappear on type. Once the user starts entering data, the label is gone. They can't double-check what the field was for. Anyone who pauses, gets distracted, or returns later loses the context entirely.
- Low contrast by default. Browser default placeholder color is gray-on-white, well below the 4.5:1 contrast ratio WCAG requires for text. Many implementations make the placeholder even lighter to "look clean."
- Autofill confusion. Browser autofill replaces the placeholder with the autofilled value. The user can't tell whether the field is empty (showing a placeholder) or filled (showing real data) without selecting it.
- Cognitive load and memory. Users have to remember what each field is for while filling it in. People with cognitive disabilities, ADHD, anxiety, or just full lives can't reliably hold ten field labels in working memory.
- Screen reader inconsistency. Some screen readers announce the placeholder as the accessible name when no label exists. Others ignore it. Others read it after the label. Behavior varies by browser, screen reader, and version.
- Translation gaps. Placeholders sometimes aren't translated by automatic translation tools that work on visible text content.
The fix
A persistent visible label above (or beside) the input. The placeholder, if used at all, should be an example of valid input — never the name of the field.
<!-- Bad -->
<input type="email" placeholder="Email address">
<!-- Good -->
<label for="email">Email address</label>
<input type="email" id="email" placeholder="you@example.com">
Note the second placeholder shows an example format; the label still names the field. The example is helpful supplementary detail; the name is essential.
// 08 · required, optional, errors
Required, Optional, Errors
The label is also where you communicate whether a field is required, optional, or in error. Each requires both visible markers and programmatic markup.
Required fields
Three things together cover everyone:
- The
requiredattribute on the input. Browsers and screen readers both use this. Native browser validation reads it; assistive tech announces "required" alongside the field name. - A visible marker. An asterisk, the word "(required)", or a clear convention. Explain the convention near the top of the form ("Fields marked * are required").
- An accessible-text version of the marker. If you use a visual asterisk that's purely decorative, append a screen-reader-only "(required)" so the announcement is unambiguous.
<label for="email">
Email address
<span aria-hidden="true" class="required-marker">*</span>
<span class="visually-hidden">(required)</span>
</label>
<input type="email" id="email" name="email" required>
Optional fields
If most fields are required, mark only the optional ones. If most are optional, mark only the required ones. Either way, the marker is consistent within a single form.
<label for="phone">
Phone number <span class="hint">(optional)</span>
</label>
<input type="tel" id="phone" name="phone">
Error association
When validation fails, the error message must be programmatically tied to the field, not just visually adjacent. Use aria-describedby pointing to the error element, plus aria-invalid="true" on the input.
<label for="email">Email address</label>
<input
type="email"
id="email"
name="email"
aria-invalid="true"
aria-describedby="email-error"
required>
<p id="email-error" class="error">
Enter a valid email address, like name@example.com.
</p>
The screen reader announces "Email address, edit, invalid entry, Enter a valid email address, like name at example dot com." The user knows the field, knows it's wrong, and knows how to fix it — without leaving the input.
Don't rely on color alone
WCAG 1.4.1 says color cannot be the only way information is conveyed. Red borders, red text, red asterisks are fine — as long as there's also a non-color signal: an icon, a word, a marker, the aria-invalid attribute. Color reinforces; it doesn't carry meaning by itself.
For dynamic error announcements after submit, see the Live Regions guide.
// 09 · common mistakes
Common Mistakes
<label>.
<label for="x"> per id="x". If two labels point to the same input, only one is read — and which one is undefined. Each input gets one label; group context comes from <fieldset>/<legend>.
title only appears on hover, never on focus, never on touch. Mouse-only and unreliable across screen readers. Some screen readers fall back to it when no other name exists; many ignore it. Don't depend on it.
<label for="x"></label> with aria-label on the input. Two sources of name, one of them empty. Most screen readers honor aria-label and ignore the label, but it's confusing to maintain. Pick one source of truth — either fill the <label> or remove it.
// 10 · decision flowchart
Decision Flowchart
When you sit down to label a field, walk through these questions in order:
-
Can you place visible text directly next to the input?
- Yes → use
<label for>with the visible text as its content. Stop. - No, but visible text already exists on screen (heading, card title, table caption) → go to step 2.
- No visible text exists and can't be added → go to step 3.
- Yes → use
-
Can you reference the existing visible text?
- Yes → use
aria-labelledbypointing to the existing element'sid. Stop. - No (it's dynamic, in a different document, etc.) → go to step 3.
- Yes → use
-
Can you add a visible label even if it feels like clutter?
- Yes → do it. Then go back to step 1. Almost always, the answer here is yes.
- No (truly impossible — strict design constraint, icon-only convention) → use
aria-label. Stop.
Most fields end at step 1. A few legitimate cases (header search, icon-only buttons) end at step 3. If you're ending at step 3 for most fields, you're using ARIA to paper over a design that doesn't show its labels — go back and add visible labels.
// 11 · testing form labels
Testing Form Labels
- Tab through the form with a screen reader on. For each field, listen for the announcement. Did you hear the label, the field type, and the state (required, invalid, etc.)? If you heard "edit blank," the label is missing.
- Click each visible label. Focus should move to (or toggle) its associated input. If clicking the label does nothing, the
for/idassociation is broken or missing. - Strip all CSS and walk the DOM. View the page with no styling. Labels should sit in source order next to their inputs. If a label appears far from its input or is missing entirely, the structure is wrong.
- Run axe-core or another automated linter. It'll catch missing labels, duplicate
ids, and brokenforreferences. Automation can't tell you whether a label is good, but it'll catch most cases of "no label." - Check device autofill suggestions. Browser autofill uses the label, the
autocompleteattribute, and thename. If autofill suggests the wrong values for a field, the label or attributes are misleading the browser. - Test with voice control. Say "Click email" or "Click submit." If the control activates, the visible label matches the accessible name. If it doesn't, you have a Label-in-Name problem (WCAG 2.5.3).
- Inspect the accessibility tree. In Chrome DevTools, the Accessibility panel shows the computed name for any element. Verify that name matches what the user sees on screen.
Automated tools catch a lot but not everything. A field labeled "Click here" passes every linter but tells the user nothing. Only manual testing catches semantic problems like that.
// 12 · wcag success criteria
WCAG Success Criteria
| Criterion | Level | How form labels apply |
|---|---|---|
| 1.3.1 Info and Relationships | A | Labels must be programmatically associated with their inputs — through <label for>, wrapping <label>, aria-labelledby, or aria-label. Visual proximity alone isn't enough. |
| 3.3.2 Labels or Instructions | A | Every input that requires user input must have a label or clear instruction explaining what's expected. |
| 4.1.2 Name, Role, Value | A | Every interactive element must have an accessible name programmatically exposed. The label provides the name. |
| 2.5.3 Label in Name | A | The accessible name must include the visible label text. If the visible label is "Submit order" and aria-label is "Place order," voice-control users saying "Click submit order" can't activate the button. |
| 1.4.1 Use of Color | A | Required-field markers and error indicators must not rely on color alone. Pair red text with an icon, marker word, or programmatic attribute (required, aria-invalid). |