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

beginner html wcag-1.3.1

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

The mental model If you turned off the screen and asked a screen reader user to fill out the form, would they know what each field is for? If the answer is no, the labels aren't doing their job.

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

Default to explicit Use <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.

When not to use fieldset A single text input with one label doesn't need a fieldset. Don't wrap every form in one. Reserve <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

  1. If you can use <label>, use <label>. It's the most reliable and gives you the click-target behavior for free.
  2. If visible text already exists and can't be moved into a <label>, use aria-labelledby. Don't duplicate text — reference it.
  3. 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).
  4. 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 is invisible 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.

// 07 · hidden but accessible labels

Hidden but Accessible Labels

Sometimes a visible label genuinely clutters the interface — a header search field, an in-table filter row, a single-input page where the label is obvious from context. In those cases, a visually hidden label is better than no label at all.

Hide the label with the .visually-hidden utility, not display: none:

<label for="header-search" class="visually-hidden">Search articles</label>
<input type="search" id="header-search" placeholder="Search…">

/* CSS */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

display: none and visibility: hidden remove the label from the accessibility tree entirely — the screen reader doesn't see it. .visually-hidden hides it visually while keeping it in the tree.

When this is OK

  • A header search where context makes the field's purpose obvious to sighted users.
  • An in-table filter input directly above the column it filters.
  • A "skip to content" link's target where the label would be redundant.

When it's not OK

If a sighted user without a screen reader would benefit from a visible label — first-time visitors, distracted users, users with cognitive disabilities, users on a small mobile screen with no surrounding context — hide nothing. Sighted assistive tech users (screen magnifier, high-contrast mode, voice control) also need visible text.

The voice-control test Voice-control users say the visible label to focus a control: "Click email." If your label is hidden, they have to guess. aria-label sometimes works, sometimes doesn't. A visible label always works.

// 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:

  1. The required attribute on the input. Browsers and screen readers both use this. Native browser validation reads it; assistive tech announces "required" alongside the field name.
  2. 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").
  3. 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

Placeholder as the only label The most common form a11y mistake. Placeholders disappear on type, fail contrast checks, confuse autofill, and are inconsistent across screen readers. Always pair an input with a real <label>.
Same id referenced by multiple labels HTML allows only one <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 attribute as a label 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.
Empty <label> then aria-label Pattern: <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.
Visible label always wins Sighted users with cognitive disabilities, low literacy, anxiety, or unfamiliarity with your iconography also benefit from a persistent visible label. Voice-control users need it to activate the field. Translation tools rely on it. The visible label serves more users than any ARIA attribute can.

// 10 · decision flowchart

Decision Flowchart

When you sit down to label a field, walk through these questions in order:

  1. 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.
  2. Can you reference the existing visible text?
    • Yes → use aria-labelledby pointing to the existing element's id. Stop.
    • No (it's dynamic, in a different document, etc.) → go to step 3.
  3. 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

  1. 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.
  2. Click each visible label. Focus should move to (or toggle) its associated input. If clicking the label does nothing, the for/id association is broken or missing.
  3. 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.
  4. Run axe-core or another automated linter. It'll catch missing labels, duplicate ids, and broken for references. Automation can't tell you whether a label is good, but it'll catch most cases of "no label."
  5. Check device autofill suggestions. Browser autofill uses the label, the autocomplete attribute, and the name. If autofill suggests the wrong values for a field, the label or attributes are misleading the browser.
  6. 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).
  7. 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).