Accessible forms and dynamic UI · 1 / 8
lesson 1

Form labels and structure

Every input needs a name. Every group needs a heading. Get this part right and the rest follows.

~ 14 min read·lesson 1 of 8
0 / 8

A form control without a label is a mystery box. Sighted users guess from the placeholder or the input next to it. A screen reader user, though, hears something like "edit, blank" — and that's it. No clue what to type. No clue why this field exists. So before we touch errors, dialogs, or any of the dynamic stuff, we have to nail the boring foundation: every control needs a name, and every related set of controls needs a group label.

This is the layer everything else sits on. If a field has no accessible name, all the aria-invalid and aria-describedby in the world won't save it.

What a label actually does

A label is a short, visible piece of text that names a form control. The <label> element is the HTML way to attach that text to an <input>, <select>, or <textarea> so the browser knows they belong together.

Two things happen when the association works:

  • Click target grows. Tapping the label text focuses the input — huge for small checkboxes and on touch screens.
  • Accessible name is set. Screen readers announce "Email, edit" instead of "edit, blank".

That accessible name — the name announced by assistive tech — is what aria-invalid, aria-describedby, and validation messages all latch onto later. No name, nothing to attach to.

Tip

Quick sanity check: tap the label text. If the input doesn't get focus, the label isn't actually associated with it.

Two ways to associate a label

There are exactly two patterns. Pick one and stay consistent.

1. for / id (explicit). The label sits anywhere; its for attribute matches the input's id.

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

2. Wrapping (implicit). The label contains the input. No id needed.

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

Both are valid. Wrapping is fewer attributes and immune to typos in id. Explicit lets the label live anywhere in the layout — useful when CSS Grid or floats decouple a label from its field.

Here's the wrong way that looks fine until a screen reader reads it:

wrong.html
<!-- DON'T: a div pretending to be a label -->
<div class="label">Email</div>
<input name="email" type="email" />

Visually identical. Programmatically, the input has no name. A screen reader will read the surrounding text and guess — sometimes well, sometimes not.

Watch out

aria-label and aria-labelledby exist as last resorts. They don't render visible text and they don't grow the click target. Reach for a real <label> first.

Grouping radios and checkboxes

A single radio button labeled "Yes" is meaningless. Yes to what? Each radio in a group needs its own label (Yes/No), but the group itself needs a heading too.

The HTML answer is <fieldset> plus <legend>. Fieldset wraps the related controls; legend is the question they all answer.

group.html
<fieldset>
<legend>Send marketing emails?</legend>

<label>
  <input type="radio" name="marketing" value="yes" />
  Yes
</label>

<label>
  <input type="radio" name="marketing" value="no" />
  No
</label>
</fieldset>

A screen reader announces this as "Send marketing emails? Yes, radio button, 1 of 2." The legend is glued to every option in the group — that's the magic.

Send marketing emails?YesNo
A fieldset gives each control inside it a shared group name. The legend is what assistive tech announces before the option.

Same pattern for checkbox groups, date pickers split into day/month/year, address blocks — anywhere multiple inputs answer one combined question.

If <legend>'s default styling is too rigid for your design, you can visually hide it (clip-path / position: absolute off-screen) and replace it with a styled heading only if the visible heading is also exposed via aria-labelledby on the fieldset. Don't quietly drop the group name.

Tip

An alternative pattern: role="group" with aria-labelledby pointing at a heading. It works, but fieldset/legend is native, semantic, and keyboard-accessible by default.

Placeholders are not labels

The grey hint text inside an empty input — that's a placeholder. It is not a label. Three problems:

  1. It disappears the moment the user types, so they can't re-read it.
  2. Its low contrast often fails WCAG color requirements.
  3. Some screen readers don't announce it at all; others announce it instead of the label.

If you want a hint, put a real label above and a hint below. The hint can live in a <small> or <p> and get wired to the input with aria-describedby (we'll use that exact pattern for errors in Lesson 3).

hint.html
<label for="username">Username</label>
<input id="username" name="username" aria-describedby="username-hint" />
<small id="username-hint">3–20 letters, no spaces.</small>

Try it yourself

check your understanding
Which markup gives the input an accessible name?
check your understanding
You have three radios — "Daily", "Weekly", "Never". What do you wrap them in?
check your understanding
Why is a placeholder a poor substitute for a label?
check your understanding
Which is the implicit (wrapping) label pattern?
next lesson →
KeepLearningcertificate
for completing
Accessible forms and dynamic UI
0 of 8 read