Tailwind CSS Form Validation — Working Code
Tailwind ships `invalid:`, `valid:`, `focus:`, and `placeholder-shown:` variants that map directly to native HTML validation pseudo-classes. No JavaScript validation library — just utility classes and the platform.
Tailwind's validation strategy: lean on the platform. Native HTML validation (`required`, `type=email`, `pattern`, `minlength`) handles the rules; Tailwind's `invalid:`, `valid:`, `focus:`, and `placeholder-shown:` variants handle the styling. Together they produce a polished error UI with zero JavaScript validation library.
The trick to delaying error styling until the user has typed is `invalid:[&:not(:placeholder-shown)]:border-red-500`. This is a Tailwind arbitrary variant: 'apply red border when the input is invalid AND the placeholder is not shown'. Combined with `placeholder=" "` (a non-breaking space, so the placeholder-shown trick works), the error state only kicks in after the user has typed something.
The `peer` utility lets sibling elements respond to the input's state. `peer-invalid:peer-[:not(:placeholder-shown)]:block` on a sibling error message means 'show this error when the peer input is invalid AND has been typed in'. Useful for inline error text that appears below the field.
Dark mode is automatic via the `dark:` variant. The form above ships both light and dark mode in the same markup; the user's OS preference decides which renders. Tailwind's `darkMode: 'media'` (the default) reads `prefers-color-scheme`; switch to `darkMode: 'class'` if you want a manual toggle.
How to set this up
Use native HTML validation attributes
required, type=email, pattern, minlength. The browser does the validation; Tailwind handles the styling.
Apply the invalid: variant
invalid:border-red-500 styles the input when invalid. Pair with :not(:placeholder-shown) to delay until typed.
Use peer for sibling error messages
peer on the input, peer-invalid: on the error message. Error text shows when the input is invalid.
Add dark mode for free
dark: variants on every color class. prefers-color-scheme decides which renders. No theme switcher needed.
invalid: + peer + dark: variants. Polished form, no JS library.
Frequently asked questions
Does Tailwind have built-in form validation?
Tailwind ships variants (invalid:, valid:, focus:) that map to native HTML validation pseudo-classes. There's no JavaScript validation library — Tailwind styles what the browser already validates.
How do I show an error message only after the user types?
Combine invalid: with :not(:placeholder-shown): `invalid:[&:not(:placeholder-shown)]:border-red-500`. Set placeholder=" " (non-breaking space) so :placeholder-shown works correctly. The error styling only kicks in after the user has typed something.
How do I do password confirmation with Tailwind?
Tailwind can't do cross-field validation — that's HTML's territory. Use 5 lines of JavaScript with setCustomValidity on the password_confirm input. Tailwind then styles the resulting invalid state automatically.
Tailwind validation vs React Hook Form — which?
Tailwind for simple forms where native validation suffices. React Hook Form + Zod for complex forms with cross-field rules, async validation, or typed schemas. Both work together — RHF for logic, Tailwind for styling.
Does Tailwind dark mode work with form validation?
Yes. Apply dark: variants on every color class — borders, backgrounds, text, focus rings. The same form ships in both light and dark mode automatically based on user OS preference.
Related guides
Ship the form, not the backend.
Free for 1,000 submissions/month. Email delivery, AI spam filtering, signed webhooks, real dashboard — all on the free plan. No credit card.
Get a free access key →