Form labels: which inputs need one and how to check
An input labelled only by its placeholder loses its label the moment someone types. squirrel lists every unlabeled input, textarea and select.
What a form label is
A <label> gives a form control its accessible name, and clicking the label moves focus into the control. Two forms work: <label for="id"> matching the control’s id, or a <label> that wraps the control.
Where a visible label does not fit the design, aria-label or aria-labelledby supplies the name instead. A placeholder is only a last-resort fallback in the name computation, and squirrel does not accept it: it disappears the moment the user types, so a form labelled that way is unusable as soon as it is filled in. Buttons, hidden inputs and image inputs are named by other means and are out of scope.
What squirrel checks
One check, form-labels, at severity error. It covers every <input> except types hidden, submit, button, reset and image, plus every <textarea> and <select>.
- Fail when a control has none of the following: an ancestor
<label>; a<label for="...">whose value matches the control’sid; anaria-label; anaria-labelledby; atitle. Report message:N form input(s) without labels. Each control is listed by itsname, falling back to itsidand then to its type. form-labels-unconfirmedwarns withN form input(s) without labels may be hidden by a style (unconfirmed)for unlabeled controls a style may hide in a way squirrel cannot settle.- Pass with
All form inputs have labels, orAll form inputs in the accessibility tree have labelswithhiddenWithoutLabelwhen unlabeled controls were skipped as hidden. The pass carriesinputsChecked. - Info with
No form inputs found.
A control out of the accessibility tree has nothing to announce, so it is not reported: one with the hidden attribute (except hidden="until-found"), aria-hidden="true", display: none on itself or an ancestor or visibility: hidden on itself or an ancestor, set inline or by a <style> rule squirrel can decide (such as .closed { display: none }), or inside a closed <dialog> or an unopened popover. It still needs a label before it can be shown. Hiding squirrel cannot settle is reported as unconfirmed instead: an inline style it cannot read exactly, a <style> rule it cannot decide, and, on a page that also loads an external stylesheet, hiding from a <style> rule or visibility inherited from an ancestor. External stylesheets are not read, so a control hidden only by the site’s external CSS, such as a closed modal, is still reported. See What squirrel can see for the details.
A page past squirrel’s style limits (more than 5,000 style rules, a <style> block over the size limit, a very long selector, a no-break or other non-ASCII space in a style block, or a rule nested inside another) is read as unconfirmed for every element a style might hide: the rules past the limit could hide or show any of them, so an unnamed control is reported as unconfirmed, not as a failure. CSS nesting is not read: a nested rule that sets display or visibility (b { @media (min-width: 0) { display: none } }, .a { & .b { display: none } }) leaves the elements it could reach unconfirmed, and a nested rule that sets neither changes nothing.
A control that carries a no-js class, itself or on an ancestor other than <html>, is reported as unconfirmed on a page with an external stylesheet or an @import, with the reason that it may be hidden when JavaScript runs. A theme usually hides that fallback with a rule such as .js .no-js { display: none } in a stylesheet squirrel does not read. The same control on a page whose styles were all read keeps its verdict, and a class such as no-js-hidden is not a marker.
A title attribute satisfies this check. It shows only on hover, so it is a weaker fix than a real label, but it does produce an accessible name.
How to fix it
<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email">The for value and the id must match exactly, and id must be unique on the page or the association silently lands on the wrong control.
A search field sitting next to a magnifying-glass button is the usual false positive to argue about. The button has a name, the input does not, and squirrel is right to report it: give the input its own aria-label.
| Rule ID | a11y/form-labels |
| Category | Accessibility |
| Scope | Per-page |
| Severity | error |
| Weight | 6/10 |
Enable / disable
Disable this rule
[rules]
disable = ["a11y/form-labels"]Disable all Accessibility rules
[rules]
disable = ["a11y/*"]Enable only this rule
[rules]
enable = ["a11y/form-labels"]enable replaces the default ["*"], so this is the only rule selected. Leave disable out: a rule that matches any disable pattern is skipped even when enable names it. Only an enabled setting under a rule’s own [rule_options] table overrides both lists.
Related rules
- a11y/aria-input-field-name: the same sweep including elements with input ARIA roles.
- a11y/select-name: dropdowns specifically.
- a11y/form-field-multiple-labels: the opposite failure, two labels on one control.
- a11y/autocomplete-tokens: the autofill token that belongs on the same field.
Accessibility findings ship in every audit next to the SEO, performance and agent experience rules. See Website security scan with AI for how an agent works through a report.
References
- The label element, MDN
- Labeling controls, W3C WAI tutorials
- H44: Using label elements to associate text labels with form controls, W3C
Check your site
Run squirrel audit https://example.com and open the Accessibility section of the report. Every unlabeled control is listed by its name attribute. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.