Home / Snippets / UI Components /

Validation states

Form inputs styled with CSS :valid, :invalid, and :user-invalid pseudo-classes — no JavaScript required.

✓ Looks good
✗ Enter a valid email address
Widely Supported
uino-js

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.