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.
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 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-300sets a subtle default borderrounded-mdgives a slight radiusshadow-smadds a barely visible shadowfocus:border-blue-500 focus:ring-blue-500replaces the ugly default focus ring with a styled onesm:text-smsizes 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>
Inline Form (Search Bar)
<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
- Always use labels. Every input needs a
<label>with a matchingfor/idpair, or wrap the input inside the label. - Error messages need
aria-describedby. Link the error<p>to the input so screen readers announce it. - Do not rely on color alone. Red borders for errors should be paired with text and/or icons.
- Keyboard navigation. Test that Tab moves through fields in order and that Enter submits the form.
- Required fields. Use
aria-required="true"or the HTMLrequiredattribute, 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.
Related articles
- Tailwind Tailwind Animation and Transition Utilities
Animate hover states, page mounts, and component transitions with Tailwind's transition and animation utilities and a few custom keyframes.
- Tailwind Design Tokens and Theming with Tailwind CSS
Build a robust theming system using custom design tokens, CSS variables, theme extension, and multi-brand support in Tailwind CSS.
- Tailwind Responsive Design Patterns with Tailwind CSS
Master mobile-first breakpoints, container queries, responsive grids, and real-world layout patterns using Tailwind CSS utilities.
- Tailwind CSS Grid Layouts with Tailwind CSS
Build powerful grid layouts using Tailwind's grid utilities, auto-fill, auto-fit, spanning, subgrid patterns, and real-world layout examples.