On this page
Forms and Accessibility
Forms and Accessibility
Forms are the primary way users interact with applications — they are also one of the most accessibility-sensitive areas of web development. This lesson covers practical Tailwind patterns for styling forms beautifully while meeting WCAG AA accessibility requirements.
Installing the forms plugin
The @tailwindcss/forms plugin provides a consistent, opinionated reset for native form elements. It removes browser-specific styling (WebKit input shadows, Firefox focus rings, platform-specific checkboxes) and gives you a clean starting point.
npm install -D @tailwindcss/forms/* In your tailwind.css */
@import "tailwindcss";
@plugin "@tailwindcss/forms";Two strategies are available:
/* Default: applies resets to native element selectors (input, select, textarea) */
@plugin "@tailwindcss/forms";
/* Class strategy: only applies resets when you add the form-input, form-select etc. classes */
@plugin "@tailwindcss/forms" { strategy: class; }The class strategy is less intrusive and is better for projects where you mix Tailwind forms with third-party UI libraries.
Input field patterns
<!-- Standard text input -->
<div class="space-y-1">
<label for="username" class="block text-sm font-medium text-gray-700">Username</label>
<input
id="username"
type="text"
autocomplete="username"
class="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm text-gray-900 placeholder-gray-400 outline-none transition-all duration-150 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/20"
placeholder="your_username"
/>
</div>
<!-- Input with leading icon -->
<div class="relative">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<svg class="w-4 h-4 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"
class="w-full border border-gray-200 rounded-lg pl-9 pr-3 py-2.5 text-sm text-gray-900 placeholder-gray-400 outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/20"
placeholder="Search..."
aria-label="Search"
/>
</div>
<!-- Input with trailing button -->
<div class="flex rounded-lg overflow-hidden border border-gray-200 focus-within:border-indigo-400 focus-within:ring-2 focus-within:ring-indigo-500/20 transition-all">
<input
type="email"
class="flex-1 px-3 py-2.5 text-sm text-gray-900 placeholder-gray-400 outline-none bg-white"
placeholder="Enter your email"
aria-label="Email for newsletter"
/>
<button
type="submit"
class="bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 transition-colors"
>
Subscribe
</button>
</div>Select and textarea
<!-- Styled select -->
<div class="relative">
<select
class="w-full appearance-none border border-gray-200 rounded-lg px-3 py-2.5 pr-8 text-sm text-gray-900 bg-white outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/20"
aria-label="Select country"
>
<option value="">Select country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="uk">United Kingdom</option>
</select>
<!-- Custom chevron icon -->
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
<!-- Textarea -->
<textarea
rows="4"
class="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm text-gray-900 placeholder-gray-400 outline-none resize-none transition-all focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/20"
placeholder="Your message..."
aria-label="Message"
></textarea>Checkboxes and radio buttons
<!-- Custom checkbox -->
<label class="flex items-center gap-3 cursor-pointer group">
<input
type="checkbox"
class="w-4 h-4 rounded border-gray-300 text-indigo-600 cursor-pointer focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 transition-colors"
/>
<span class="text-sm text-gray-700 group-hover:text-gray-900 transition-colors">
I agree to the Terms of Service
</span>
</label>
<!-- Radio group -->
<fieldset>
<legend class="text-sm font-semibold text-gray-900 mb-3">Notification preference</legend>
<div class="space-y-2">
<label class="flex items-center gap-3 cursor-pointer">
<input type="radio" name="notif" value="email" class="w-4 h-4 text-indigo-600 border-gray-300 focus-visible:ring-2 focus-visible:ring-indigo-500" />
<span class="text-sm text-gray-700">Email only</span>
</label>
<label class="flex items-center gap-3 cursor-pointer">
<input type="radio" name="notif" value="push" class="w-4 h-4 text-indigo-600 border-gray-300 focus-visible:ring-2 focus-visible:ring-indigo-500" />
<span class="text-sm text-gray-700">Push notifications</span>
</label>
<label class="flex items-center gap-3 cursor-pointer">
<input type="radio" name="notif" value="both" class="w-4 h-4 text-indigo-600 border-gray-300 focus-visible:ring-2 focus-visible:ring-indigo-500" />
<span class="text-sm text-gray-700">Email and push</span>
</label>
</div>
</fieldset>Focus visible and keyboard accessibility
The focus-visible: variant applies styles only when focus is triggered by keyboard navigation, not mouse clicks. This gives keyboard users a clear focus indicator without the unwanted focus ring on every mouse click.
<!-- Good: keyboard focus visible, mouse click focus invisible -->
<button class="outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 bg-indigo-600 text-white px-4 py-2 rounded-lg">
Save
</button>
<!-- Links too -->
<a href="/about" class="text-indigo-600 hover:text-indigo-800 outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 rounded">
About us
</a>Ring utilities for focus indicators
The ring-* utilities create inset box-shadows that serve as accessible focus indicators:
<!-- Basic ring -->
<input class="focus:ring-2 focus:ring-indigo-500" />
<!-- Ring with offset (gap between element edge and ring) -->
<button class="focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2">...</button>
<!-- Custom ring color and width -->
<button class="focus-visible:ring-4 focus-visible:ring-indigo-500/50">...</button>
<!-- Ring that adapts in dark mode -->
<button class="focus-visible:ring-2 focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400">...</button>Disabled states
<!-- Input disabled -->
<input
type="text"
disabled
class="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm text-gray-400 bg-gray-50 cursor-not-allowed outline-none"
value="Read-only value"
/>
<!-- Button disabled -->
<button
type="button"
disabled
class="bg-indigo-600 text-white font-semibold px-5 py-2.5 rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
Submit
</button>Visually hidden (screen reader only)
The sr-only class hides content visually while keeping it accessible to screen readers:
<!-- Icon button with accessible label -->
<button type="button" class="p-2 rounded-lg hover:bg-gray-100 focus-visible:ring-2 focus-visible:ring-indigo-500">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<span class="sr-only">Close dialog</span>
</button>
<!-- Skip navigation link (visible only on focus) -->
<a
href="#main-content"
class="sr-only focus-visible:not-sr-only focus-visible:fixed focus-visible:top-4 focus-visible:left-4 focus-visible:z-50 focus-visible:bg-white focus-visible:px-4 focus-visible:py-2 focus-visible:rounded-lg focus-visible:ring-2 focus-visible:ring-indigo-500 font-medium text-indigo-700"
>
Skip to main content
</a>Validation states with peer
Using the peer variant, you can show or hide validation messages based on input validity without JavaScript:
<div class="space-y-1">
<label for="user-email" class="block text-sm font-medium text-gray-700">Email</label>
<input
id="user-email"
type="email"
required
class="peer w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/20 invalid:border-red-400 invalid:focus:ring-red-500/20"
placeholder="[email protected]"
/>
<!-- Error message: visible only when input is invalid AND was interacted with -->
<p class="text-xs text-red-600 hidden peer-invalid:peer-not-placeholder-shown:block">
Please enter a valid email address.
</p>
</div>Practice
- Build a complete sign-up form with fields for name, email, password, and a terms checkbox. Every field must have a visible
<label>, correctaria-required, and afocus-visible:ring-2focus indicator. - Add a "Skip to main content" skip link at the top of the page using
sr-onlyandfocus-visible:not-sr-only. - Create a custom toggle switch using a checkbox with the
peervariant to change the toggle track color when checked.
<!-- Fully accessible form with Tailwind styling -->
<form class="max-w-lg mx-auto bg-white rounded-2xl shadow-md border border-gray-100 p-8 space-y-6" novalidate>
<div>
<h2 class="text-xl font-bold text-gray-900">Contact us</h2>
<p class="text-sm text-gray-500 mt-1">We'll respond within 24 hours.</p>
</div>
<!-- Name field -->
<div class="space-y-1">
<label for="full-name" class="block text-sm font-medium text-gray-700">
Full name <span class="text-red-500" aria-hidden="true">*</span>
</label>
<input
id="full-name"
name="full_name"
type="text"
required
autocomplete="name"
aria-required="true"
class="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm text-gray-900 placeholder-gray-400 outline-none transition-all duration-150 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/20 invalid:border-red-400 invalid:focus:ring-red-500/20"
placeholder="Alice Johnson"
/>
</div>
<!-- Email field -->
<div class="space-y-1">
<label for="email-address" class="block text-sm font-medium text-gray-700">
Email address <span class="text-red-500" aria-hidden="true">*</span>
</label>
<input
id="email-address"
name="email"
type="email"
required
autocomplete="email"
aria-required="true"
aria-describedby="email-hint"
class="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm text-gray-900 placeholder-gray-400 outline-none transition-all duration-150 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/20 invalid:border-red-400"
placeholder="[email protected]"
/>
<p id="email-hint" class="text-xs text-gray-400">We will never share your email.</p>
</div>
<!-- Subject select -->
<div class="space-y-1">
<label for="subject" class="block text-sm font-medium text-gray-700">Subject</label>
<select
id="subject"
name="subject"
class="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm text-gray-900 bg-white outline-none transition-all duration-150 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/20"
>
<option value="">Select a subject</option>
<option value="general">General inquiry</option>
<option value="support">Technical support</option>
<option value="billing">Billing question</option>
</select>
</div>
<!-- Message textarea -->
<div class="space-y-1">
<label for="message" class="block text-sm font-medium text-gray-700">
Message <span class="text-red-500" aria-hidden="true">*</span>
</label>
<textarea
id="message"
name="message"
rows="4"
required
aria-required="true"
class="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm text-gray-900 placeholder-gray-400 outline-none resize-none transition-all duration-150 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/20"
placeholder="Tell us how we can help..."
></textarea>
</div>
<!-- Checkbox -->
<div class="flex items-start gap-3">
<input
id="newsletter"
name="newsletter"
type="checkbox"
class="mt-0.5 w-4 h-4 rounded border-gray-300 text-indigo-600 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
/>
<label for="newsletter" class="text-sm text-gray-700 cursor-pointer">
Subscribe to our newsletter for updates and tips.
</label>
</div>
<!-- Submit button -->
<button
type="submit"
class="w-full bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white font-semibold py-3 rounded-xl transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2"
>
Send message
</button>
</form>
Sign in to track your progress