HTMLworkingevergreen

Native form validation

The browser already validates for you.

Constraint attributes, the Constraint Validation API, and :invalid styling give you accessible, no-library form validation that blocks bad submissions before any JavaScript runs.

Constraints are attributes

Every constraint is a plain HTML attribute on the control: required, minlength/maxlength, min/max/step, pattern, and the input type itself (email, url, number, tel). When a control violates a constraint, the form refuses to submit and the browser shows a built-in bubble pointing at the first offender. None of this needs JavaScript β€” it works even if your scripts fail to load.

The type attribute carries semantics and validation: type="email" requires an @-shaped string, type="url" requires an absolute URL, and type="number" rejects non-numeric input while enabling min/max/step.

Styling with :invalid and :user-invalid

:invalid and :valid match controls by their current constraint state, so you can colour borders or show messages purely in CSS. The catch is that :invalid matches a required empty field immediately on load β€” before the user has touched anything. The modern fix is :user-invalid, which only matches after the user has interacted with the field (or attempted to submit), giving you red borders at the right moment instead of an angry form on first paint.

The Constraint Validation API

Each control exposes a validity object (a ValidityState) with boolean flags like valueMissing, typeMismatch, patternMismatch, rangeOverflow, and tooShort. checkValidity() returns whether the control passes, and setCustomValidity(msg) marks a control invalid with your own message β€” pass an empty string to clear it. This is how you express rules HTML attributes can't, such as "these two passwords must match," while still hooking into the native bubble and submit-blocking.

Try it 5 examples

required + :user-invalid styling

HTML+CSS+JSintro
Press Run to execute

Both fields paint with the neutral #2a2218 border on load β€” :user-invalid deliberately holds off β€” so the required name field only turns red (#e0533d) once you focus and leave it empty or hit Save, while a filled field turns green via :user-valid. The optional bio field never reddens because it has no constraint to violate.