Home / Snippets / UI Components /
Validation states
Form inputs styled with CSS :valid, :invalid, and :user-invalid pseudo-classes — no JavaScript required.
Quick implementation
.field-input {
background: var(--card);
border: 1.5px solid oklch(0.35 0.02 260);
border-radius: 0.5rem;
padding: 0.625rem 0.875rem;
font-size: 0.95rem;
color: var(--text);
outline: none;
width: 100%;
box-sizing: border-box;
transition: border-color 0.2s;
}
.field-input:focus {
border-color: var(--accent);
}
/* Valid state — fires after user interaction */
.field-input:user-valid {
border-color: oklch(0.7 0.18 145);
}
/* Invalid state — only after user has touched the field */
.field-input:user-invalid {
border-color: oklch(0.65 0.22 25);
}
/* Hint text colours */
.field-hint--valid { color: oklch(0.7 0.18 145); font-size: 0.78rem; }
.field-hint--invalid { color: oklch(0.65 0.22 25); font-size: 0.78rem; }
Prompt this to your LLM
Includes role, constraints, two framework variants, and edge cases to handle.
You are a senior frontend engineer building a CSS form validation system.
Goal: Style form inputs with three states — default, valid, and invalid — using CSS pseudo-classes only. No JavaScript.
Technical constraints:
- Use :user-valid and :user-invalid (not :valid/:invalid) so styles only apply after the user has interacted with the field.
- Valid state: border-color oklch(0.7 0.18 145) (green).
- Invalid state: border-color oklch(0.65 0.22 25) (red).
- Default/focus state: border-color uses var(--accent).
- Include transition: border-color 0.2s on all inputs.
- Use oklch() for all color values, not hex or rgba().
Framework variant (pick one):
A) Vanilla CSS — apply to any input with the .field-input class.
B) React component — wraps a label, input, and optional hint span; accepts type, label, required, and errorMessage props.
Edge cases to handle:
- :user-invalid only fires after blur or form submission — do not rely on :invalid for live styling.
- Colour alone must not be the only indicator — include a hint text element with ✓ / ✗ prefix.
- required + empty should show invalid only after the user has touched the field.
Return HTML and CSS.
Why this matters in 2026
Native CSS validation pseudo-classes have existed for years, but :user-invalid — which only activates after the user has actually interacted with a field — arrived in all major browsers in 2023. Before it, developers had to add a JavaScript blur listener to apply an "touched" class. Now the browser handles it natively, reducing form validation boilerplate significantly.
The logic
:valid and :invalid match as soon as the page loads based on the input's current value and constraints — unhelpful for empty required fields that haven't been touched. :user-valid and :user-invalid solve this by only firing after the user has modified the field or attempted to submit the form. The border colour transitions between three states: neutral (oklch(0.35 0.02 260)), focused (var(--accent)), valid (oklch(0.7 0.18 145)), and invalid (oklch(0.65 0.22 25)). The oklch colour space keeps green and red perceptually balanced in lightness.
Accessibility & performance
Never rely on colour alone to communicate validation state — add a visible hint text with a ✓ or ✗ icon prefix so colour-blind users can read the outcome. Use aria-describedby on the input pointing to the hint element so screen readers announce the message on focus. For programmatic validation (e.g., server-side errors), add aria-invalid="true" manually — CSS :invalid does not set this attribute automatically. The transition on border-color is a paint operation but very cheap on small elements.