20 CSS Form Validation

20 hand-coded CSS form validation patterns built on native HTML5 constraint validation:user-invalid so a required field is not red before anyone has typed, inline error messages that reserve their space, character counters, confirm-password match, required checkbox groups, and a :has() summary box. Every error state pairs colour with an icon and text, and ships aria-invalid plus aria-describedby. Scoped under .fv-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.

11 pure CSS9 light JSPublished

Related28 CSS Input Fields16 CSS Floating Label Inputs30 CSS Login Forms20 CSS Loading Buttons

Modern User Invalid Form Fields — preview
01 / 20Pure CSS

Modern User Invalid Form Fields

A side-by-side comparison of the two ways to style a failing field. The left column uses bare :invalid and is already red with nothing typed, because an empty required field is invalid the instant it renders. The right column uses :user-invalid and stays neutral until the field is blurred or a submit is attempted. Reach for this pattern first — it decides whether the rest of your validation styling is helpful or hostile.

Published

Required Field Indicator and Legend — preview
02 / 20Pure CSS

Required Field Indicator and Legend

Required and optional marking done properly. The asterisk is decorative and carries aria-hidden="true", the real signal lives in the field's accessible name, and a legend explains the convention once at the top instead of leaving users to guess. A second panel shows the inverse convention — marking the optional fields — which reads better when nearly every field is mandatory.

Published

Inline Field Level Error Messages — preview
03 / 20Pure CSS

Inline Field Level Error Messages

The error message sits directly under its input and animates open without a magic max-height. A grid-template-rows transition from 0fr to 1fr lets the message take its natural height, and the row is reserved in layout so revealing it never pushes the rest of the form down. Driven entirely by :user-invalid — no script, no class toggling.

Published

Pure CSS Required Checkbox Groups — preview
04 / 20Light JS

Pure CSS Required Checkbox Groups

"Select at least one" is the one common requirement native HTML cannot express. required on a radio applies to the whole group; on a checkbox it applies only to that single box, so ticking a different option leaves the form invalid. This demo puts both groups side by side and closes the checkbox gap with a nine-line setCustomValidity() shim, keeping the verdict inside the native constraint system.

Published

Placeholder Shown Empty Field State — preview
05 / 20Pure CSS

Placeholder Shown Empty Field State

:placeholder-shown answers one question well — is this field still empty — and it is the backbone of the pre-:user-invalid validation pattern. This demo lays out the four states side by side (empty, empty and focused, abandoned, filled), shows why :not(:focus) matters, and spells out the two limits: it needs a non-empty placeholder attribute to ever match, and empty is not the same as invalid.

Published

Success Checkmark on Valid Input — preview
06 / 20Pure CSS

Success Checkmark on Valid Input

A checkmark that draws itself in when the field becomes valid, driven by :user-valid and a stroke-dasharray animation. The technique is easy; the judgement is the point. Confirming every field turns a form into a scoreboard, so this demo confirms only the two fields where success is genuinely news — an available username and a verified card postcode — and leaves the rest silent.

Published

Confirm Password Match Validation — preview
07 / 20Light JS

Confirm Password Match Validation

Two password fields and one question: do they match. Fifteen lines call setCustomValidity() on the confirm field whenever either input changes, which hands the verdict to the browser's own constraint system — so :user-invalid styles it, checkValidity() respects it, and the native message says the right thing. The match indicator between the fields updates live; the error message waits for blur.

Published

Character Counter Limit Warning — preview
08 / 20Light JS

Character Counter Limit Warning

A live count under a <textarea> with maxlength, moving through three bands: normal, approaching in the last ten percent, and at the limit. The count lives in an <output> with tabular-nums so the digits do not jitter, and the announcement fires once when a band is crossed — never on every keystroke, which is how counters become unusable with a screen reader.

Published

Custom Select Required Validation — preview
09 / 20Pure CSS

Custom Select Required Validation

required on a <select> only works when the placeholder option has an empty value — that one detail is what most implementations miss. This demo pairs value="" with disabled selected so the unchosen state is genuinely invalid from first paint, then styles it with bare :invalid, which is safe here in a way it is nowhere else in this collection.

Published

Input Focus Ring with Error State — preview
10 / 20Pure CSS

Input Focus Ring with Error State

Focus and error have to coexist. An invalid field that gains focus must show both signals, and the usual failure is a red border swallowing the focus ring so keyboard users lose their place. This demo lays out the full three-state matrix — valid and focused, invalid and blurred, invalid and focused — using outline rather than box-shadow so Windows High Contrast Mode keeps the ring.

Published

Accessible Error with ARIA Describedby — preview
11 / 20Light JS

Accessible Error with ARIA Describedby

The full accessible-error contract in one field group: aria-invalid="true" on the control, aria-describedby pointing at the message's id, and the message removed from the accessibility tree the moment the field becomes valid. An inspector panel beside the form shows the computed name, role, state and description for the focused field, so you can see what a screen reader would actually receive.

Published

Validation Summary Box with Has Selector — preview
12 / 20Light JS

Validation Summary Box with Has Selector

A summary panel above the form that appears only while the form contains a field the user has left invalid — .fv-12__form:has(:user-invalid) .fv-12__summary, and nothing else. Each line links to its field by id so clicking moves focus straight there. The script only keeps the list text current; whether the panel is on screen is decided entirely in CSS, and that split is the point.

Published

Error Announcement with Live Region — preview
13 / 20Light JS

Error Announcement with Live Region

role="alert" and aria-live="polite" are not interchangeable. A submit-time summary interrupts, because the user has just asked for something and needs the answer; a per-field error on blur waits its turn. This demo runs both, side by side, and prints every announcement into a visible transcript so you can inspect the behaviour without turning a screen reader on.

Published

High Contrast Validation Focus States — preview
14 / 20Pure CSS

High Contrast Validation Focus States

Validation that survives forced-colors: active and does not lean on saturated colour to carry meaning. The error signal here is weight, an icon and a rule — colour is the last layer, not the first. Inside a @media (forced-colors: active) block the states switch to the Mark, MarkText and Highlight system keywords so the browser's own palette drives them.

Published

Minimalist Checkout Field Validation — preview
15 / 20Light JS

Minimalist Checkout Field Validation

Card number, expiry and CVC in a dense three-field grid. Format comes from pattern and inputmode="numeric", and a Luhn check runs through setCustomValidity() so the native constraint system delivers the verdict. The autocomplete tokens are the detail most checkout demos get wrong — cc-number, cc-exp and cc-csc are what make browser autofill work at all.

Published

Compact Signup Form Validation — preview
16 / 20Light JS

Compact Signup Form Validation

A complete signup form in a 380px column that validates without feeling hostile: errors appear on blur, never on keystroke, and the submit button is never disabled. A disabled button gives the user nothing to press and no explanation of why — so this one stays live, reports what is wrong, and moves focus to the first field that needs work.

Published

Newsletter Email Pattern Validation — preview
17 / 20Pure CSS

Newsletter Email Pattern Validation

A single email field inline with its button, and an honest account of what type="email" does and does not check. The native type accepts a@b — no dot required — so a pattern tightens it to something with a real domain. The demo also argues the other side: over-strict email regexes reject plus-addressing, long new TLDs and unicode local parts, and those rejections cost you real subscribers.

Published

Date Range Validation Min and Max — preview
18 / 20Light JS

Date Range Validation Min and Max

Start and end dates where the end must fall after the start. Absolute bounds come free from min and max, and :out-of-range styles a value outside them — a pseudo-class most developers never meet. The relative constraint needs about twenty lines that move the end field's min whenever the start changes, with the verdict routed through setCustomValidity().

Published

Dark Mode Validation Colour Tokens — preview
19 / 20Pure CSS

Dark Mode Validation Colour Tokens

The error red that clears 4.5:1 on white usually fails on a dark ground — same hue, half the contrast. This demo defines four semantic tokens (--fv-error, --fv-error-bg, --fv-success, --fv-warning), swaps them under prefers-color-scheme: dark and a [data-theme="dark"] override, and prints the measured contrast ratio next to every swatch so the change is auditable rather than assumed.

Published

No JS Form Submit Blocked Visuals — preview
20 / 20Pure CSS

No JS Form Submit Blocked Visuals

What the browser does on its own when a required field is empty and submit is pressed: it blocks the submission, focuses the first invalid field and shows its bubble. This demo styles the surrounding experience with :has() so the form visibly reacts before any of that happens, and spells out what novalidate actually turns off — the blocking and the bubbles, but never the CSS pseudo-classes, which keep matching.

Published

FAQ

Frequently asked questions

What is the difference between :invalid and :user-invalid in CSS?
Bare :invalid matches the moment the page loads. An empty required field is invalid from the very first paint, so a form styled with :invalid renders every field red before the visitor has typed a single character — which reads as broken rather than as helpful. :user-invalid fixes exactly this: it matches only after the user has actually interacted with the field, meaning after they have typed and blurred, or after a submit attempt. The name is the specification being literal — the field is invalid as far as the user's own input is concerned. Support is Chrome 119, Safari 16.5 and Firefox 88, and the classic fallback for older engines is :invalid:not(:placeholder-shown), which approximates the same behaviour by refusing to match while the field is still empty. Declare that rule first and let :user-invalid override it. Demo 01 in this collection shows both side by side so the difference is visible rather than theoretical.
How do I show an error message without the layout shifting?
Reserve the space before the message exists. If the error element only appears in the DOM when validation fails, everything below it moves down at exactly the moment the user is trying to read it — and that is a Cumulative Layout Shift event as well as a usability problem. The cleanest modern approach is a grid row that animates from 0fr to 1fr: the message lives in a grid child with overflow: hidden, and the row size transitions between those two values, so it expands smoothly without a magic max-height number that you have to guess and then maintain. Where you can rely on it, interpolate-size: allow-keywords lets you transition to auto directly, though Safari and Firefox have not shipped it, so keep the 0fr path as the base. The blunt alternative that always works is simply giving the message container a fixed min-height sized to one line of text — less elegant, but it never shifts, and for a single-line message it is perfectly reasonable.
How do I make form validation accessible to screen readers?
Four things, and they work together. Put aria-invalid="true" on the field while it is in an error state and remove it when the field becomes valid — that is what tells assistive technology the control itself is wrong, not just that some red text exists nearby. Link the field to its message with aria-describedby pointing at the message element's id, so the error is read out as part of the field rather than as orphaned text. Put the message after the input in DOM order, because a screen reader announces the description after the label and value, and a message placed before the field can be announced out of sequence. Finally, never signal an error with colour alone — that fails WCAG 1.4.1 for colour-blind users and for anyone in bright sunlight. Every error state in this collection pairs the colour with an icon and with text. If you are announcing errors dynamically rather than on a page load, you also need a live region, and the critical detail there is that the region must already exist in the DOM before you write into it.
Should form errors appear as the user types or after they finish?
After they finish. Validating on every keystroke means the field turns red while someone is halfway through typing their email address, which is both wrong and hostile — the value is not invalid, it is incomplete. The convention that works is to validate on blur for a field the user has already filled, and on submit for everything else. Once a field has been marked invalid, it is reasonable to switch that individual field to live re-validation so the error clears the instant the user fixes it, which feels responsive rather than punishing. :user-invalid gives you the blur-based half of this behaviour for free with no JavaScript at all, which is a large part of why it is worth the browser-support floor. The one case for immediate feedback is a character counter approaching a hard limit, because there the information is genuinely useful before the user finishes — and even then, announce it once at the threshold rather than on every keystroke.
How do I make at least one checkbox required in a group?
Native HTML cannot express it. The required attribute on a checkbox means that specific box must be checked, which is correct for a single "I accept the terms" control but useless for "pick at least one option". Radio buttons behave differently — there, required on any member applies to the whole group, because a radio group is a single value by definition. For checkboxes you need a small shim, and the right way to write it is with setCustomValidity() rather than a CSS class: pick one member of the group as the carrier, and on every change set a non-empty message on it when nothing is checked and an empty string when something is. Passing an empty string is what clears the error — passing null does not, and that mistake leaves the field permanently invalid, which is one of the more baffling bugs to debug. The payoff is that the state lives inside the browser's own constraint system, so :user-invalid, checkValidity() and the native submit blocking all understand it without any further work.
Should I disable the submit button until the form is valid?
No, in almost every case. A disabled submit button is a dead end: it gives the user nothing to click and no explanation of what is wrong, so they are left hunting for the offending field with no feedback loop. It is also an accessibility problem, because a disabled element is removed from the tab order and drops focus silently. The better pattern is to leave the button enabled and let the submit attempt do the work — the browser focuses the first invalid field and shows its message, or your handler calls reportValidity() and does the same thing. The user gets a specific answer to "why can't I continue?" instead of a greyed-out rectangle. There is a narrow exception for destructive or irreversible actions where you genuinely want to prevent an accidental activation, but for a signup form, a checkout, or a contact form, an always-enabled button that explains itself on click is both more usable and more accessible.
Can I do form validation with CSS only, without JavaScript?
For styling the states, yes — and that is most of what people actually want. The browser's constraint validation runs regardless of whether you write any JavaScript, so :user-invalid, :user-valid, :placeholder-shown, :required, :out-of-range and friends all match on their own, and :has() lets a parent react to a child's state so you can style a whole field wrapper or reveal a summary panel without a line of script. What CSS cannot do is express a constraint the platform has no attribute for: comparing two fields to each other, checking one value against another field's value, or anything requiring a computed rule such as a Luhn check. Those need setCustomValidity(), which is a handful of lines and keeps the verdict inside the native system. It is also worth knowing what novalidate does and does not turn off — it stops the browser blocking submission and suppresses the built-in bubbles, but the CSS pseudo-classes keep matching, so you can take over the messaging entirely while keeping every state style intact.
How do these port to React, Vue, Svelte and Tailwind?
The CSS ports unchanged — every .fv-NN selector and custom property is framework-agnostic, and eleven of the twenty demos need no JavaScript at all. The part worth rethinking is that React-era form code often reimplements validation in state, tracking a touched flag per field and a parallel error object, when :user-invalid already encodes "touched and invalid" in the platform. Leaning on the native constraint system means less state to synchronise and fewer ways for the UI to disagree with the actual validity of the form. Where you do need custom rules, call setCustomValidity() in an effect or handler and let the pseudo-classes style the result. Vue and Svelte follow the same shape with a ref or a plain binding. For Tailwind, the state variants map directly — user-invalid: is available as a modifier, so the error styling stays in markup alongside everything else, and the arbitrary-variant syntax covers :has() where you need a parent to react.

Related collections

15 CSS 3D Tilt Hover Cards preview

15 CSS 3D Tilt Hover Cards

15 mouse-aware 3D tilt hover cards — e-commerce product spotlight, glassmorphism parallax team card, interactive pricing tier, holographic NFT collectible, pop-out mascot, dark tech grid with border glow, media player album art, blog article preview, cyberpunk neon glow, dashboard KPI widget, minimalist real estate, flip-to-back tilt, mobile app showcase, course learning card, and a Pure CSS touch-friendly tilt. Vanilla JS writes --rx/--ry/--mx/--my; all rendering stays in CSS on the GPU.

24 CSS Animated Cards preview

24 CSS Animated Cards

24 hand-coded CSS animated card patterns organised by what triggers the motion, not by card style. Scroll and entrance triggers: staggered grid reveals, @starting-style mount-in, scroll-driven scale and fade on animation-timeline: view(), View Transitions API card-to-detail morphs, blur-to-sharp lazy image loading, and FLIP re-layout when a filter changes. State-change triggers: add-to-cart success morph, like and save particle burst, expand and collapse to intrinsic height with calc-size(), swipe-to-dismiss on Pointer Events, and skeleton-to-loaded crossfade. Data-driven triggers: count-up KPI metrics, SVG sparkline draw-on-enter, and progress ring fill on load. Ambient idle loops: breathing glow, floating drift, animated gradient mesh backgrounds, conic rotating borders, auto-cycling testimonial decks, and live status pulses. Plus four hover patterns where the effect is the topic itself: cursor spotlight, holographic foil glare, corner ribbon slide, and horizontal accordion expand. Every card is scoped under a .ac-NN prefix for no-collision pasting, guards prefers-reduced-motion, animates compositor-only properties, and ports unchanged to React, Vue, Svelte, Astro, Next.js and Tailwind.

22 CSS Avatars preview

22 CSS Avatars

22 hand-coded CSS avatars for chat apps, team dashboards, comment threads, account menus, and social profiles. Covers circular and squircle shapes, gradient and conic story rings, hexagon clip-path crops, online status dots, notification count badges, verified checkmarks, stacked facepiles with plus-N overflow, initials fallbacks for users without photos, broken-image recovery, avatar pickers, and a size scale system where the ring, badge, and status dot all scale from a single custom property.

Search CodeFronts

Loading…