skip to content

In CSS, why do :invalid styles appear on empty required fields the moment a form renders, and which selector avoids that?

level: seniorimportance: should knowfreq 35%

answer

  1. a state mirror, not an event log
  2. empty plus required fails at parse time
  3. interaction-gated variant exists
  4. placeholder-shown as the older gate
  5. touched class was the library workaround

basics

~20 s

:invalid reflects constraint-validation state continuously, so an empty required field is invalid from the moment it is parsed — before anyone types. Use :user-invalid, which matches only after the user has interacted with the field or attempted submission.

solid answer

~50 s

`:invalid` is a live mirror of the control's constraint-validation state, not a record of what the user did. An empty required field fails its constraint as soon as it exists, so an error style attached to `:invalid` paints the whole form red on first render — which reads as an accusation before the user has typed a character. The fix is `:user-invalid`, which matches only once the user has interacted with the control and moved on, or once submission has been attempted; it has a `:user-valid` counterpart for success styling. Where the support floor is too high, the older workaround is `input:not(:placeholder-shown):invalid`, which suppresses the style while the field is empty — it requires the control to carry a placeholder. The last resort is a class applied on blur or submit from script, which is what most form libraries did before `:user-invalid` was broadly available.

code

css · 9 lines
css
/* fires on load for an empty required field */
input:invalid { border-color: crimson; }

/* preferred: waits for interaction or a submit attempt */
input:user-invalid { border-color: crimson; }
input:user-valid   { border-color: seagreen; }

/* older fallback: only once something has been typed */
input:not(:placeholder-shown):invalid { border-color: crimson; }

go deeper

for a junior

Know that :invalid matches from the moment the page loads for an empty required field, and that :user-invalid is the selector meant for showing errors after interaction.

for a middle

Explain the cause — these pseudo-classes mirror constraint state continuously and carry no interaction history — and describe the :not(:placeholder-shown):invalid fallback along with its dependency on a placeholder attribute.

for a senior

Show the production judgment: choose when feedback appears (on blur versus on first submit), keep the error in text rather than colour alone, and pick between the platform selector and a script-applied class based on your support floor.

for a principal

Own the form-feedback contract across products — one timing rule, one error presentation, encoded once in the design system — so that individual teams are not each inventing when a field is allowed to turn red.

## Why the styles fire immediately The form-validity pseudo-classes describe *state*, and state exists from the moment the document is parsed. `:invalid` matches a control whose value fails its constraints right now — an empty field marked required fails immediately, before any interaction. There is no notion of "the user has had a chance yet" built into it. So the naive rule ```css input:invalid { border-color: crimson; } ``` renders a page of red boxes on load. That is not a bug in CSS; it is a mismatch between what `:invalid` means (constraint state) and what the designer wanted (error *feedback*, which is inherently about interaction history). ## The interaction-aware pseudo-classes `:user-invalid` and `:user-valid` were added precisely for this. They match the same validity state but only after the user has interacted with the control — typically after editing and blurring it, or after a submission attempt — so a pristine field matches neither. ```css input:user-invalid { border-color: crimson; } input:user-valid { border-color: seagreen; } ``` This is the right default today. Support is the only caveat: Firefox and Safari shipped them earlier, and Chrome and Edge added them in version 119 in late 2023, so treat them as broadly available from that point and verify against your own support floor. ## The placeholder-shown workaround Before those landed, the standard trick used another state pseudo-class: ```css input:not(:placeholder-shown):invalid { border-color: crimson; } ``` `:placeholder-shown` matches while the control is displaying its placeholder text, which happens exactly when the field is empty — so negating it means "has something typed in it". Combined with `:invalid`, the error style waits until the user has entered *something* wrong rather than nothing at all. Its limitations are worth stating: it requires the control to have a placeholder attribute at all, it still fires mid-typing (an email is invalid until the moment it is complete), and it says nothing about a required field the user tabbed straight past. It is a workaround, not a replacement. ## The script-assisted pattern The most controllable approach, and what most form libraries do, is to keep validity styling behind a state class: ```css .field.is-touched input:invalid { border-color: crimson; } ``` with the class added on blur or on submit. The advantage is that the definition of "the user has had their chance" is yours: per field on blur, or the whole form on first submit, which is usually the gentler UX. The cost is script and a state to keep in sync — which is exactly the cost `:user-invalid` removes. ## The rest of the validity family - `:valid` / `:invalid` — the current constraint state, from parse time. - `:user-valid` / `:user-invalid` — the same, gated on interaction. - `:required` / `:optional` — whether a value must be supplied, useful for marking labels without duplicating the information in a class. - `:in-range` / `:out-of-range` — for numeric and date controls with bounds. - `:placeholder-shown` — the placeholder is currently displayed. - `:read-only` / `:read-write` — whether the value is editable. Note that `:invalid` also applies to the `form` element itself, matching whenever any control inside it fails validation, which is a convenient hook for disabling a summary or styling a banner. ## Judgment an interviewer is listening for Three things separate a strong answer. First, name the underlying cause — the pseudo-class is a state mirror, not an event log — rather than just reciting the fix. Second, note that colour alone is not an accessible error indicator: the message must be conveyed in text associated with the field, and CSS only decorates it. Third, prefer validating on blur and on submit rather than on every keystroke, because live invalidation while someone is halfway through typing an email address is hostile; whichever mechanism you choose, that timing decision is the actual design question, and `:user-invalid` simply encodes the common answer in the platform.

  • What exactly does :placeholder-shown match, and why does the fallback depend on it?
    It matches a control while its placeholder text is on screen, which is the case only when the field is empty and unfocused-or-untyped. Negating it therefore means "has a value", so `:not(:placeholder-shown):invalid` withholds the error until the user has entered something. It fails on controls with no placeholder attribute at all.
  • Does :invalid apply to anything other than individual controls?
    Yes — a `form` element matches `:invalid` whenever any control inside it fails validation, and `:valid` when they all pass. That makes it a handy hook for styling a submit banner or a summary region without tracking each field, though the same premature-feedback caveat applies on first render.
  • Why is styling the border red not a sufficient error treatment?
    Colour alone excludes users who cannot perceive the distinction and conveys nothing to a screen reader. The error must exist as text associated with the field, with the CSS acting only as reinforcement — and the indicator should differ in more than hue, for example a shape, icon, or weight change alongside the colour.
  • When would you still reach for a script-applied class instead of :user-invalid?
    When the timing rule is not the platform's default — for example showing errors only after the first submit attempt for the whole form, rather than per field on blur, or when a server-side validation result has no client-side constraint to mirror. The class also gives you a single hook that both cases can share.

:invalid is a smoke alarm that starts beeping the moment it is installed because nobody has cooked yet; :user-invalid waits until you have actually used the kitchen.

saying these in an interview costs you the question

  • Thinks :invalid only matches after the user types
  • Believes required is what makes it match late
  • Uses :invalid on load and calls it correct feedback
  • Treats a red border alone as an accessible error
  • Validates on every keystroke as the default

context