CSSForm State Pseudo-Classes (:checked, :disabled, :valid)

Form State Pseudo-Classes (:checked, :disabled, :valid)

HTML form controls carry a lot of built-in state — checked or not, enabled or not, required or not, passing HTML5 validation or not. CSS exposes all of this directly as pseudo-classes, so you can style a form's current state without touching JavaScript.

The pseudo-classes

Pseudo-class

Matches when...

:checked

A checkbox or radio (or a <select> option) is currently checked/selected.

:disabled

A form control has the disabled attribute and cannot be interacted with.

:enabled

A form control is capable of being interacted with (the default state).

:required

A field has the required attribute.

:optional

A field does NOT have the required attribute.

:valid

A field's current value passes HTML5 validation constraints.

:invalid

A field's current value fails HTML5 validation constraints.

:in-range

A numeric/date field's value is within its min/max bounds.

:out-of-range

A numeric/date field's value falls outside its min/max bounds.

:checked

CSS
input[type="checkbox"]:checked {
  accent-color: #2563eb;
}

/* Style a custom "toggle" label based on the hidden checkbox's state */
input.toggle:checked + label {
  background: #22c55e;
}
:disabled / :enabled

CSS
button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

input:enabled:hover {
  border-color: #2563eb;
}
:required / :optional and :valid / :invalid

CSS
/* Mark required fields visually */
input:required {
  border-left: 3px solid #f59e0b;
}

/* Numeric range field */
input[type="number"] {
  border: 1px solid #ddd;
}

input[type="number"]:out-of-range {
  border-color: crimson;
  background: #fff5f5;
}
Worked example: styling invalid input, carefully

A naive approach shows a red border the instant an input is invalid — which usually means it flashes red before the user has even typed anything, since an empty required field is technically :invalid.

CSS
/* Naive: shows red immediately on page load */
input:invalid {
  border-color: crimson;
}

A common improvement is to only flag invalid state once the field has actually been interacted with, using a class toggled by JavaScript on blur:

CSS
/* Only show the error state after the field has been
   touched (a JS-toggled class), not on first render */
input.touched:invalid {
  border-color: crimson;
  background: #fff5f5;
}

input.touched:valid {
  border-color: #22c55e;
}
Note
Newer browsers support `:user-invalid` and `:user-valid`, which behave like `:invalid`/`:valid` but only start matching AFTER the user has interacted with the field — solving the "red border on page load" problem natively, with no JavaScript or extra classes required. Support is improving but not yet universal, so check current compatibility before relying on it as your only mechanism.