Skip to content
Codeloom
Tailwind

Styling Forms and Inputs with Tailwind CSS

A complete guide to styling forms, text inputs, selects, checkboxes, radios, and validation states using Tailwind CSS utilities.

·7 min read · By Codeloom
Intermediate 13 min read

What you'll learn

  • How to style text inputs, textareas, and selects with Tailwind utilities
  • How to customize checkboxes and radio buttons without extra libraries
  • How to build accessible form layouts with labels and error states
  • How to use the @tailwindcss/forms plugin for sensible defaults
  • How to handle focus, disabled, and validation states

Prerequisites

  • Familiarity with Tailwind CSS utility classes
  • Basic HTML form knowledge

Forms are the most under-styled part of most websites. Browser defaults look different on every OS, inputs are notoriously difficult to customize, and checkboxes have resisted CSS for decades. Tailwind CSS gives you the utilities to style every form element consistently, and the @tailwindcss/forms plugin resets browser defaults so you start from a clean slate.

The Problem with Default Form Styles

Every browser renders inputs differently. A <select> on Chrome looks nothing like one on Safari. A checkbox on Windows is different from one on macOS. If you want consistent styling, you have to reset these defaults.

Without @tailwindcss/forms:
- Each browser applies its own styles
- Inputs have different padding, borders, fonts
- Checkboxes and radios are nearly impossible to style

With @tailwindcss/forms:
- All form elements get a clean baseline
- Utility classes apply predictably
- You build up from a normalized starting point
Without reset vs with reset

Install the Forms Plugin

npm install @tailwindcss/forms
// tailwind.config.js
export default {
  plugins: [
    require('@tailwindcss/forms'),
  ],
};

The plugin resets all form elements to unstyled defaults that respond to Tailwind utilities. It does not add any visual design — it just removes browser inconsistencies.

Text Inputs and Textareas

Start with a basic text input:

<label class="block">
  <span class="text-sm font-medium text-gray-700">Email address</span>
  <input
    type="email"
    placeholder="you@example.com"
    class="mt-1 block w-full rounded-md border-gray-300 shadow-sm
           focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
  />
</label>

Key classes to understand:

  • border-gray-300 sets a subtle default border
  • rounded-md gives a slight radius
  • shadow-sm adds a barely visible shadow
  • focus:border-blue-500 focus:ring-blue-500 replaces the ugly default focus ring with a styled one
  • sm:text-sm sizes the text appropriately

For textareas, the same classes work:

<label class="block">
  <span class="text-sm font-medium text-gray-700">Message</span>
  <textarea
    rows="4"
    placeholder="Write your message..."
    class="mt-1 block w-full rounded-md border-gray-300 shadow-sm
           focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
  ></textarea>
</label>

Select Dropdowns

The native <select> element:

<label class="block">
  <span class="text-sm font-medium text-gray-700">Country</span>
  <select class="mt-1 block w-full rounded-md border-gray-300 shadow-sm
                 focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
    <option value="">Select a country</option>
    <option value="us">United States</option>
    <option value="uk">United Kingdom</option>
    <option value="ca">Canada</option>
  </select>
</label>

The forms plugin automatically adds a custom dropdown arrow. If you want to replace it, use appearance-none and add a background SVG or an absolutely positioned icon.

Checkboxes and Radio Buttons

These are historically the hardest elements to style. Tailwind makes them manageable:

<!-- Checkbox -->
<label class="flex items-center gap-2">
  <input
    type="checkbox"
    class="h-4 w-4 rounded border-gray-300 text-blue-600
           focus:ring-blue-500"
  />
  <span class="text-sm text-gray-700">I agree to the terms</span>
</label>

<!-- Radio buttons -->
<fieldset class="space-y-2">
  <legend class="text-sm font-medium text-gray-700">Plan</legend>
  <label class="flex items-center gap-2">
    <input
      type="radio"
      name="plan"
      value="free"
      class="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
    />
    <span class="text-sm text-gray-700">Free</span>
  </label>
  <label class="flex items-center gap-2">
    <input
      type="radio"
      name="plan"
      value="pro"
      class="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
    />
    <span class="text-sm text-gray-700">Pro ($10/mo)</span>
  </label>
</fieldset>

The text-blue-600 class on checkboxes and radios sets the checked color. The rounded class makes a checkbox rounded (omit it for a square). Radio buttons are always circular.

Toggle Switch

Build a custom toggle without JavaScript, using a hidden checkbox and peer utilities:

<label class="relative inline-flex cursor-pointer items-center">
  <input type="checkbox" class="peer sr-only" />
  <div class="h-6 w-11 rounded-full bg-gray-200 peer-checked:bg-blue-600
              peer-focus:ring-4 peer-focus:ring-blue-300
              after:absolute after:left-[2px] after:top-[2px]
              after:h-5 after:w-5 after:rounded-full after:bg-white
              after:transition-all after:content-['']
              peer-checked:after:translate-x-full">
  </div>
  <span class="ml-3 text-sm text-gray-700">Enable notifications</span>
</label>

The sr-only class visually hides the checkbox but keeps it accessible to screen readers. The peer-checked: prefix applies styles to the sibling when the checkbox is checked.

Form Layout Patterns

Stacked Form

The simplest layout, works well on mobile:

<form class="mx-auto max-w-md space-y-6 p-6">
  <div>
    <label for="name" class="block text-sm font-medium text-gray-700">Full name</label>
    <input id="name" type="text" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" />
  </div>
  <div>
    <label for="email" class="block text-sm font-medium text-gray-700">Email</label>
    <input id="email" type="email" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" />
  </div>
  <div>
    <label for="password" class="block text-sm font-medium text-gray-700">Password</label>
    <input id="password" type="password" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" />
  </div>
  <button type="submit" class="w-full rounded-md bg-blue-600 px-4 py-2 text-white font-semibold hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
    Create account
  </button>
</form>
<form class="flex gap-2">
  <input
    type="search"
    placeholder="Search articles..."
    class="flex-1 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
  />
  <button type="submit" class="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
    Search
  </button>
</form>

Two-Column Form

<form class="space-y-6">
  <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
    <div>
      <label class="block text-sm font-medium text-gray-700">First name</label>
      <input type="text" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" />
    </div>
    <div>
      <label class="block text-sm font-medium text-gray-700">Last name</label>
      <input type="text" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" />
    </div>
  </div>
  <div>
    <label class="block text-sm font-medium text-gray-700">Email</label>
    <input type="email" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" />
  </div>
</form>

Validation States

Use Tailwind’s state variants for error, success, and disabled states:

<!-- Error state -->
<div>
  <label class="block text-sm font-medium text-red-700">Email</label>
  <input
    type="email"
    value="not-an-email"
    class="mt-1 block w-full rounded-md border-red-300 text-red-900
           placeholder-red-300 focus:border-red-500 focus:ring-red-500 sm:text-sm"
  />
  <p class="mt-1 text-sm text-red-600">Please enter a valid email address.</p>
</div>

<!-- Success state -->
<div>
  <label class="block text-sm font-medium text-green-700">Email</label>
  <input
    type="email"
    value="user@example.com"
    class="mt-1 block w-full rounded-md border-green-300 text-green-900
           focus:border-green-500 focus:ring-green-500 sm:text-sm"
  />
  <p class="mt-1 text-sm text-green-600">Looks good!</p>
</div>

<!-- Disabled state -->
<input
  type="text"
  disabled
  value="Cannot edit this"
  class="mt-1 block w-full rounded-md border-gray-200 bg-gray-100
         text-gray-500 shadow-sm sm:text-sm cursor-not-allowed"
/>

For HTML5 validation with the :invalid pseudo-class:

<input
  type="email"
  required
  class="rounded-md border-gray-300 invalid:border-red-500 invalid:text-red-600
         focus:invalid:border-red-500 focus:invalid:ring-red-500"
/>

Input Groups (Addons)

Attach icons or text to inputs:

<!-- Prefix text -->
<div class="flex rounded-md shadow-sm">
  <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500">
    https://
  </span>
  <input
    type="text"
    placeholder="www.example.com"
    class="block w-full rounded-none rounded-r-md border-gray-300 focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
  />
</div>

<!-- Icon inside input -->
<div class="relative">
  <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
    <svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
    </svg>
  </div>
  <input
    type="search"
    placeholder="Search..."
    class="block w-full rounded-md border-gray-300 pl-10 focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
  />
</div>

File Inputs

Style the file input using the file: variant:

<input
  type="file"
  class="block w-full text-sm text-gray-500
         file:mr-4 file:rounded-md file:border-0 file:bg-blue-50
         file:px-4 file:py-2 file:text-sm file:font-semibold
         file:text-blue-700 hover:file:bg-blue-100"
/>

Accessibility Reminders

  1. Always use labels. Every input needs a <label> with a matching for/id pair, or wrap the input inside the label.
  2. Error messages need aria-describedby. Link the error <p> to the input so screen readers announce it.
  3. Do not rely on color alone. Red borders for errors should be paired with text and/or icons.
  4. Keyboard navigation. Test that Tab moves through fields in order and that Enter submits the form.
  5. Required fields. Use aria-required="true" or the HTML required attribute, and visually mark them.
<div>
  <label for="email-field" class="block text-sm font-medium text-gray-700">
    Email <span class="text-red-500">*</span>
  </label>
  <input
    id="email-field"
    type="email"
    required
    aria-required="true"
    aria-describedby="email-error"
    class="mt-1 block w-full rounded-md border-red-300 focus:border-red-500 focus:ring-red-500 sm:text-sm"
  />
  <p id="email-error" class="mt-1 text-sm text-red-600" role="alert">
    Email is required.
  </p>
</div>

Wrap-Up

Install @tailwindcss/forms to reset browser defaults, then build up with utility classes. Use focus:ring and focus:border for clear focus states, peer-checked: for custom checkboxes and toggles, and validation variants for error handling. Keep forms accessible with proper labels, error associations, and keyboard support. Consistent form styling is one of the highest-impact improvements you can make to a UI.