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 invalid:focus:dark:ring-red-500 invalid:focus:dark:ring-opacity-30;
  @apply invalid:dark:bg-red-800/20 invalid:dark: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;
    @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 invalid:focus:dark:ring-red-500 invalid:focus:dark:ring-opacity-30;
  @apply invalid:dark:bg-red-800/20 invalid:dark:border-red-900;
  @apply invalid:bg-red-50 invalid:border-red-300;
}

https://play.tailwindcss.com/awVeicebKX