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.
- 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:
- Mobile keyboards closer to the data (email @, tel pad).
- Built-in validation hooks (
typeMismatchfor email/url). - Browser password managers and autofill (with good
name/autocomplete). - 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:
- Keep the label stable; put errors in text linked via
aria-describedby. - Toggle
aria-invalid="true"when the field fails. - Don’t rely on color alone for errors (WCAG POUR).
- 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
nameon 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:
- Start from native controls when possible.
- Follow ARIA APG patterns for keyboard and roles.
- Keep a programmatic name (label or
aria-labelledby). - Mirror visible focus for keyboard users.
Common mistakes
- Placeholder-only fields.
- Clickable
<div>“inputs” without roles/names. - Label text that doesn’t match the actual question (visible “Phone” vs name “fax”).
- Missing
autocompleteon auth/checkout. - Errors only as toast, never tied to the field.
forpointing at a missing or duplicateid.- 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.
Related on this site
- Links vs buttons
- dialog element
- WCAG principles (POUR)
- Accessible forms and errors
- Form validation attributes
- Focus management
- XSS for frontend engineers — never reflect raw field values into HTML sinks
Further reading
- MDN: How to structure a web form
- MDN: The HTML form element
- W3C WAI: Forms tutorials
- HTML Living Standard — forms
Related guides
- Autocomplete and Name Attributesname and autocomplete on form fields — password managers, autofill tokens, and why missing names break real users more than demos.
- The dialog ElementNative modal and non-modal dialogs with showModal, light dismiss, focus, and the top layer — without a heavyweight widget library.
- fieldset and legendGroup related form controls with fieldset/legend — radio groups, disabled fieldsets, and accessible naming for control sets.
- Form Validation AttributesNative constraint validation — required, type, pattern, min/max, minlength/maxlength, and wiring :invalid with accessible errors.
- Links vs ButtonsNavigate with links, act with buttons — correct semantics, keyboard behavior, and the SPA footguns that break both.