Forms with Real Validation States

Style focus, required, invalid and disabled states — with a floating label, no JS.

Good forms communicate. Tailwind's state variants let the input itself tell the user what is happening, without a line of JavaScript.

The states worth styling

  • focus: — a clear ring: focus:ring-2 focus:ring-cyan-300 focus:border-cyan-500.
  • required + invalid: / user-invalid: — a red border once the field is wrong.
  • disabled: — dimmed and cursor-not-allowed.

Floating labels with peer

Place the input first with a placeholder, mark it peer, then position the label and animate it on peer-focus and peer-[:not(:placeholder-shown)]:

<input placeholder=" " class="peer…">
<label class="peer-focus:-translate-y-4 peer-focus:text-cyan-600…">Email</label>

Show errors at the right time

Plain invalid: fires the instant the field is empty, which nags. Prefer user-invalid: (styles only after the user has interacted), or gate a peer error message on it, so validation feels calm rather than accusatory.

Example

Try it yourself
Loading editor…
Press Run to see the result.

When to use it

  • A login form highlights the email field with a cyan ring on focus and turns the border red via the invalid: variant when the user submits an empty value, with no JavaScript required.
  • A checkout form disables and grays out the billing address fields when a "Same as shipping" checkbox is toggled, using the disabled: variant to communicate unavailability.
  • A sign-up form uses a floating label pattern — the label slides up and shrinks when the input has a value — implemented with the peer utility and CSS transitions, zero JS.

More examples

Input with focus and invalid states

Uses focus: and invalid: variants so the border and ring change color automatically based on browser-native validation state — no JS event listeners needed.

Example · html
<div class="flex flex-col gap-1">
  <label for="email" class="text-sm font-medium text-gray-700">Email</label>
  <input
    id="email" type="email" required placeholder="[email protected]"
    class="rounded-lg border border-gray-300 px-3 py-2 text-sm
           focus:outline-none focus:ring-2 focus:ring-cyan-300 focus:border-cyan-500
           invalid:border-red-400 invalid:ring-red-200
           placeholder:text-gray-400"
  />
</div>

Disabled field with visual cue

disabled:opacity-60 and cursor-not-allowed communicate non-interactivity without custom CSS, matching native browser disabled behaviour visually.

Example · html
<fieldset class="flex flex-col gap-4">
  <div class="flex flex-col gap-1">
    <label for="name" class="text-sm font-medium text-gray-700">Full name</label>
    <input id="name" type="text"
           class="rounded-lg border border-gray-300 px-3 py-2 text-sm
                  focus:outline-none focus:ring-2 focus:ring-cyan-300 focus:border-cyan-500" />
  </div>
  <div class="flex flex-col gap-1">
    <label for="promo" class="text-sm font-medium text-gray-400">Promo code</label>
    <input id="promo" type="text" disabled placeholder="N/A for your plan"
           class="rounded-lg border border-gray-200 px-3 py-2 text-sm bg-gray-50
                  text-gray-400 cursor-not-allowed disabled:opacity-60" />
  </div>
</fieldset>

Floating label with peer utility

The peer class on the input exposes its state to the sibling label; peer-placeholder-shown and peer-focus move and shrink the label with a CSS transition — pure HTML/Tailwind, no JS.

Example · html
<div class="relative mt-6">
  <input
    id="fname" type="text" placeholder=" "
    class="peer w-full rounded-lg border border-gray-300 px-3 pt-5 pb-2 text-sm
           focus:outline-none focus:ring-2 focus:ring-cyan-300 focus:border-cyan-500"
  />
  <label for="fname"
         class="absolute left-3 top-3 text-gray-400 text-sm transition-all duration-200
                peer-placeholder-shown:top-3 peer-placeholder-shown:text-sm
                peer-focus:top-1 peer-focus:text-xs peer-focus:text-cyan-600">
    First name
  </label>
</div>

Discussion

  • Be the first to comment on this lesson.
Forms with Real Validation States — Tailwind CSS | SoundsCode