Browsers run form validation natively; most teams then re-implement its visuals in JavaScript. CSS can style the machinery directly, which suits a static site perfectly.
/* touched, wrong */
input:user-invalid { border-color: #bb0000; }
/* untouched but already empty by design */
input:placeholder-shown { border-color: #d9d9d9; }
/* the whole form is submittable */
form:has(:invalid) .save { opacity: .4; pointer-events: none; }Compose the states honestly
- Wrong value: :user-invalid, so fields never scream before the reader has touched them.
- Whole-form ready: form:has(:invalid) disables the save button until every required field passes.
- Empty-vs-error: give inputs a placeholder attribute so :placeholder-shown separates 'empty' from 'invalid'.
- Error text: pair the styling with a visible message; a red border alone fails a11y checks.
Keep semantics real: the browser also blocks submit natively, so the CSS is paint on top of judged behavior, not a replacement. The :has() support story per caniuse measured 95.91 percent in this repo, and a form still validates correctly without it.
