squirrelscan
GitHub

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’s id; an aria-label; an aria-labelledby; a title. Report message: N form input(s) without labels. Each control is listed by its name, falling back to its id and then to its type.
  • form-labels-unconfirmed warns with N 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, or All form inputs in the accessibility tree have labels with hiddenWithoutLabel when unlabeled controls were skipped as hidden. The pass carries inputsChecked.
  • 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 IDa11y/form-labels
CategoryAccessibility
ScopePer-page
Severityerror
Weight6/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.

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

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.

Type to search…

↑↓ navigate↵ openesc close