ESC

Type to search the knowledge base.

Forms, Labels, and Inputs

Wire labels to controls correctly, choose input types, group fields, and avoid the accessibility bugs that fail real users and audits.

beginner4 min read
  • html
  • forms
  • labels
  • inputs
  • accessibility

Forms are where products collect money, accounts, and PII. Broken labels are not a “nice to have” — they break screen readers, hurt accuracy of voice control and autofill, and fail WCAG Name, Role, Value expectations.

Docs: MDN Forms, MDN <label>, MDN <input>, WAI forms tutorial.

Label every control

A control needs an accessible name. The robust, default path is a <label> associated with the control.

Explicit association (for / id)

<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" />

for must match the control’s id. Clicking the label focuses/activates the control — larger hit target for free.

Implicit association (wrapping)

<label>
  Email
  <input name="email" type="email" autocomplete="email" />
</label>

Wrapping works without id, but complex layouts (label far from field, grid placement) often need explicit for/id. Pick one pattern and be consistent.

What is not a label

<!-- Visible text that is NOT programmatically tied -->
<span class="label">Email</span>
<input type="email" name="email" />

<!-- Placeholder is not a label -->
<input type="email" placeholder="Email" />

Placeholder disappears on input, has weaker contrast, and is a poor substitute for a persistent label. Use it for format hints only, if at all.

aria-label / aria-labelledby can name controls when a visible label truly cannot exist (icon-only search). Prefer visible labels for most fields — they help everyone.

Name, role, value (mental model)

Assistive tech exposes:

Piece Typical source
Name <label>, aria-label, aria-labelledby, button text
Role Native element (textbox, checkbox, …) or ARIA role
Value Current value / checked state

Native inputs give role and keyboard behavior. Rebuilding a checkbox with <div onClick> means you own name, role, value, focus, and keys. Rarely worth it.

Input types that pay rent

<input type="email" inputmode="email" autocomplete="email" />
<input type="tel" autocomplete="tel" />
<input type="url" />
<input type="number" inputmode="decimal" /> <!-- or text + inputmode for full control -->
<input type="password" autocomplete="current-password" />
<input type="search" />
<input type="date" />

Benefits of correct types:

  1. Mobile keyboards closer to the data (email @, tel pad).
  2. Built-in validation hooks (typeMismatch for email/url).
  3. Browser password managers and autofill (with good name / autocomplete).
  4. Clearer semantics for AT.

inputmode tweaks the keyboard without changing validation the way type does. autocomplete tokens are a product feature — don’t leave them off on login, checkout, or address forms. See autocomplete and name.

Required, describedby, and errors

<label for="username">Username</label>
<input
  id="username"
  name="username"
  required
  aria-required="true"
  aria-invalid="false"
  aria-describedby="username-hint username-error"
/>
<p id="username-hint">3–20 characters, letters and numbers.</p>
<p id="username-error" hidden class="error" role="alert">
  Username is required.
</p>

Patterns that hold up:

  1. Keep the label stable; put errors in text linked via aria-describedby.
  2. Toggle aria-invalid="true" when the field fails.
  3. Don’t rely on color alone for errors (WCAG POUR).
  4. Move focus to the first invalid field on submit failure when it helps the task.

HTML constraint validation (required, minlength, pattern, setCustomValidity) works with native :invalid styling. Custom UI should still update accessible names/descriptions — not only red borders.

Groups: radio, checkbox, fieldset

<fieldset>
  <legend>Shipping method</legend>
  <label><input type="radio" name="ship" value="standard" /> Standard</label>
  <label><input type="radio" name="ship" value="express" /> Express</label>
</fieldset>
  • Same name on radios = one tab stop group, arrow-key selection.
  • <fieldset> + <legend> names the group for AT.
  • Don’t use fieldset for arbitrary layout chrome; use it for related controls.

Buttons inside forms

<button type="submit">Save</button>
<button type="button" id="add-row">Add row</button>
<button type="reset">Reset</button> <!-- rarely what product wants -->

Default type for <button> inside a form is submit. A “cancel” or “add another” button without type="button" will submit and reload — classic bug. Prefer links vs buttons for the navigation vs action split.

Labels with custom widgets

If you compose a combobox or date picker:

  1. Start from native controls when possible.
  2. Follow ARIA APG patterns for keyboard and roles.
  3. Keep a programmatic name (label or aria-labelledby).
  4. Mirror visible focus for keyboard users.

Common mistakes

  1. Placeholder-only fields.
  2. Clickable <div> “inputs” without roles/names.
  3. Label text that doesn’t match the actual question (visible “Phone” vs name “fax”).
  4. Missing autocomplete on auth/checkout.
  5. Errors only as toast, never tied to the field.
  6. for pointing at a missing or duplicate id.
  7. Disabling focus outlines on invalid fields “for aesthetics.”

Interview angle

Explain explicit vs implicit labels. Why placeholder isn’t a label. How radios share name and why fieldset/legend matter. One submit-button type footgun. Mention aria-describedby for hints/errors.

Live coding: accessible login form (email, password, submit) with autocomplete tokens and a validation error associated to the field.

Further reading

Related guides