Form validation with Tailwind
Tailwind have multiple utilities for different inputs states like required
, invalid
, and disabled
. See the pseudo-class reference for a complete list of available pseudo-class modifiers.
But what if user just loaded website and there are required
inputs with default (empty) value? They will be immediately red and invalid
- that’s not what you expect!
<input type="url" class="invalid:bg-red-500" value="" required />
You should wait with input validation to user. That’s why the CSS pseud-class
:user-invalid
exists. This class represents any validated form element whose value isn’t valid based on their validation constraints, after the user has interacted with it. Right now :user-invalid
pseudo-class isn’t very well
supported - works only in Firefox.
Let’s crate following Javascript (sort of polyfill):
/** * Form validation for older browsers * * @see https://developer.mozilla.org/en-US/docs/Web/CSS/:user-invalid * @see https://caniuse.com/?search=user-invalid */try { if (typeof window !== "undefined") { document.querySelector(":user-invalid"); }} catch { document.addEventListener("DOMContentLoaded", () => { for (const input of document.querySelectorAll("input")) { input.addEventListener("change", (event) => event.target.classList.add("dirty"), ); } });}
This code adds a dirty
class to all changed input fields, which helps us to simulate the :user-invalid
pseudo-class and clearly mark all inputs that have been changed by the user. Then you can easily distinguish between changed and unchanged inputs that are invalid.
Following Tailwind code requires tailwindcss-forms plugin:
/* invalid and dirty or :user*/:is(input:user-invalid, input.dirty) { @apply invalid:focus:ring-red-100 dark:invalid:focus:ring-red-500 dark:invalid:focus:ring-opacity-30; @apply dark:invalid:bg-red-800/20 dark:invalid:border-red-900; @apply invalid:bg-red-50 invalid:border-red-300;}
and full CSS example:
@tailwind base;
@layer base { [type="text"], [type="email"], [type="url"], [type="password"], [type="number"], [type="date"], [type="datetime-local"], [type="month"], [type="search"], [type="tel"], [type="time"], [type="week"], [multiple], textarea, select { @apply block w-full rounded-md shadow-sm; @apply border-gray-300 focus:border-blue-300; @apply dark:bg-gray-700 dark:border-gray-600 dark:text-white; @apply placeholder-gray-500 dark:placeholder-gray-400; @apply dark:border-gray-600 dark:focus:border-gray-500;
@a,pply focus:ring-3; @apply focus:ring-blue-200 dark:focus:ring-gray-600; @apply focus:ring-opacity-50 dark:focus:ring-opacity-50; }}
@tailwind components;@tailwind utilities;
/* invalid and dirty or :user*/:is(input:user-invalid, input.dirty) { @apply invalid:focus:ring-red-100 dark:invalid:focus:ring-red-500 dark:invalid:focus:ring-opacity-30; @apply dark:invalid:bg-red-800/20 dark:invalid:border-red-900; @apply invalid:bg-red-50 invalid:border-red-300;}