HTML forms · 3 / 9
lesson 3

Labels and associations

label for, wrapping labels, fieldset and legend — the markup that tells screen readers, password managers, and your future self what each input is.

~ 14 min read·lesson 3 of 9
0 / 9

A <label> is the text that names a control. Without it, an input has no name — and an input with no name is invisible to screen readers, useless to password managers, and fragile to maintain.

This lesson covers the two ways to attach a label to a control, when to group related controls under a <fieldset>, and the rare cases where ARIA is the right escape hatch.

Two ways to attach a label

Two patterns, both correct, both equally accessible:

Explicit (for + id) — the label points to its input by id.

explicit.html
<label for="email">Email</label>
<input id="email" name="email" type="email">

The for attribute holds the input's id. Click the label and the input focuses. A screen reader on the input announces "Email, edit text".

Implicit (wrapping) — the input lives inside the label.

implicit.html
<label>
Email
<input name="email" type="email">
</label>

No ids needed. The connection is the nesting itself.

Pick whichever fits your layout:

  • Explicit is more flexible — the label and the input can be in different parts of a CSS grid layout (label in a sidebar, input in a column).
  • Implicit is shorter and harder to break — you cannot accidentally set the wrong for value because there is no for to set wrong.

Stay consistent within a project. Mixing both in one form is fine; mixing them with no pattern is harder to maintain.

broken.html
<!-- broken: label is not connected to anything -->
<div>Email</div>
<input name="email" type="email">

<!-- broken: for points to a non-existent id -->
<label for="emial">Email</label>
<input id="email" name="email" type="email">

The first looks identical on screen but a screen reader announces the input as "edit, blank". The second is the typo trap of explicit labels — the for value misses the input by one character.

Tip

When you click a real label and the input doesn't focus, the connection is broken. That click test is faster than reading the markup and catches almost every label-association bug. Also tap the label on a phone — touch targets are bigger when the label is a click target.

check your understanding
You add <label>Email</label><input name="email"> — sibling label, no for. The label is next to the input visually. Is the input labelled?

fieldset and legend

Some controls only make sense in a group: radio buttons for "shipping speed", checkboxes for "topics you're interested in", a billing address with several fields. The HTML element for "this is a group of controls" is <fieldset>. The element that names the group is <legend>.

fieldset.html
<fieldset>
<legend>Shipping speed</legend>

<label><input type="radio" name="speed" value="standard" checked> Standard (5-7 days)</label>
<label><input type="radio" name="speed" value="express" > Express (2-3 days)</label>
<label><input type="radio" name="speed" value="next" > Next day</label>
</fieldset>

Each radio has its own label ("Standard", "Express", "Next day"). The <legend> ("Shipping speed") names the group — a screen reader announces "Shipping speed, Standard, radio button, 1 of 3 selected" so the user hears the group name with each option.

Browsers draw a faint border around the fieldset and position the legend on the border. Most designers restyle this — border: none and a margin-aware legend is common — but the structure stays correct underneath.

Wrap an address group in a fieldset:

address.html
<fieldset>
<legend>Billing address</legend>

<label>Street <input name="street"></label>
<label>City <input name="city"></label>
<label>Postcode <input name="postcode"></label>
</fieldset>

The legend is the heading of the group. A screen reader user navigating by group hears "Billing address, group" before each field name. Without the fieldset, the fields sit naked in the form's flow with no grouping.

When not to use a fieldset: when the controls are not a group. A standalone email input does not need one. A login form with email + password does not need one (every login screen looks like a fieldset in the visual sense, but two unrelated text fields are not a "group of related controls" in the semantic sense). Save fieldset for genuine sets — radios, checkbox groups, multi-field address blocks.

Watch out

Radio buttons that share a name always belong in a fieldset with a legend. The legend is what tells the user what the group is for; without it, screen readers announce "radio button, 1 of 3" with no context. The visual heading above the radios is not a substitute — it is not connected.

check your understanding
You build a "Topics you're interested in" section with five checkboxes (bread, pastry, sourdough, brunch, coffee). The visible heading "Topics you're interested in" is an <h3> above the checkboxes. A screen-reader user lands on "bread". What do they hear?

aria-label as last resort

Sometimes there is no visible label by design. A search box where the magnifying-glass icon is the only visible affordance. A "close" button in a dialog where the X icon is the visible label. For those, ARIA gives you two attributes:

  • aria-label="..." — the label is the string you provide. Not visible; only screen readers see it.
  • aria-labelledby="id-of-element" — points to another element on the page that holds the label.
aria-label.html
<!-- search box with no visible label -->
<input type="search" name="q" aria-label="Search the bakery">

<!-- close button with only an icon -->
<button aria-label="Close dialog">
<svg aria-hidden="true" viewBox="0 0 16 16">
  <path d="M3 3 L13 13 M13 3 L3 13" stroke="currentColor" />
</svg>
</button>

The reader hears "Search the bakery, search edit text" or "Close dialog, button". The icon is decorative (aria-hidden="true" from lesson 7 of semantic-html).

The order of preference, from best to worst:

  1. A visible <label> — works for everyone, all the time.
  2. A visually-hidden <label> — the label is in the markup and styled off-screen with CSS (a class often called .sr-only or .visually-hidden). Screen readers see it; sighted users do not.
  3. aria-labelledby — point at an existing visible heading or text.
  4. aria-label — a string only assistive tech can see.

The hidden-label approach (option 2) is usually better than aria-label because the label lives in the page where translation tools and content editors can find it. aria-label is invisible to most translators.

visually-hidden.html
<style>
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0);
  border: 0;
}
</style>

<label class="sr-only" for="q">Search the bakery</label>
<input id="q" type="search" name="q">

The label is in the DOM, in the page's language, fully translatable. Sighted users see only the search box; screen-reader users hear "Search the bakery". The .sr-only class is a standard CSS utility — copy-paste from any starter project.

Placeholders are not labels

The placeholder attribute is example text inside an empty input. People reach for it to save space — drop the visible label and let the placeholder describe the field. That breaks accessibility in three ways:

  1. It vanishes the moment the user starts typing. If they forget what the field was for, there is no way back.
  2. It is greyed out — often below contrast standards, hard to read for users with low vision.
  3. Screen readers may or may not announce it. Inconsistent across browsers and tools.
broken.html
<!-- broken: placeholder doing the label's job -->
<input type="email" placeholder="Email" name="email">

Add a real label, and use placeholder only for an example value:

fixed.html
<label for="email">Email</label>
<input id="email" type="email" name="email" placeholder="you@example.com">

The label is the field name. The placeholder shows the shape of a valid value. Both audiences are served.

Watch out

If you absolutely need a "floating label" design (label sits inside the input, animates above on focus), implement it with a real <label> and CSS transforms — not with placeholder. The label has to stay in the DOM throughout, even when it's animated to look like it's gone.

check your understanding
You design a search box where, to save space, you only show a placeholder ("Search…") and no visible label. The simplest accessible fix is:
check your understanding
You have a five-radio-button group for shipping speed inside a <fieldset> with a <legend>. Each radio also has its own <label>. A screen-reader user tabs onto "Standard". What do they hear?
check your understanding
An icon-only "delete" button in your toolbar uses an SVG trash icon and no visible text. The right markup is:
← prevnext lesson →
KeepLearningcertificate
for completing
HTML forms
0 of 9 read