Forms & @tailwindcss/forms
Tailwind’s form utilities + @tailwindcss/forms plugin normalise browser styles and let you compose accessible inputs. Standard pieces: input, label, error message, focus ring.
Inputs, labels, errors, plugin
EXAMPLE
<!-- npm i -D @tailwindcss/forms -->
<!-- module.exports = { plugins: [require('@tailwindcss/forms')] } -->
<!-- 1) Text input with label + error -->
<div class="space-y-1">
<label for="email" class="block text-sm font-medium text-slate-700">
Email
</label>
<input
id="email"
type="email"
autocomplete="email"
class="block w-full rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm"
aria-describedby="email-error"
aria-invalid="true"
/>
<p id="email-error" class="text-sm text-rose-600">
Please enter a valid email
</p>
</div>
<!-- 2) Password with show/hide -->
<div class="space-y-1">
<label for="password" class="block text-sm font-medium">Password</label>
<div class="relative">
<input
id="password"
type="password"
class="block w-full rounded-md border-slate-300 pr-10 focus:border-sky-500 focus:ring-sky-500"
/>
<button type="button" class="absolute inset-y-0 right-0 px-3 text-slate-500">
👁
</button>
</div>
</div>
<!-- 3) Textarea -->
<textarea
rows="4"
class="block w-full rounded-md border-slate-300 focus:border-sky-500 focus:ring-sky-500"
placeholder="Your message…"
></textarea>
<!-- 4) Select -->
<select class="block w-full rounded-md border-slate-300 focus:border-sky-500 focus:ring-sky-500">
<option>Sydney</option>
<option>Melbourne</option>
<option>Brisbane</option>
</select>
<!-- 5) Checkbox -->
<label class="flex items-start gap-2">
<input
type="checkbox"
class="mt-1 h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500"
/>
<span class="text-sm text-slate-700">
I accept the <a href="/terms" class="text-sky-600 underline">terms</a>
</span>
</label>
<!-- 6) Radio group -->
<fieldset class="space-y-2">
<legend class="text-sm font-medium">Plan</legend>
<label class="flex items-center gap-2">
<input name="plan" type="radio" class="h-4 w-4 border-slate-300 text-sky-600 focus:ring-sky-500" />
<span class="text-sm">Free</span>
</label>
<label class="flex items-center gap-2">
<input name="plan" type="radio" class="h-4 w-4 border-slate-300 text-sky-600 focus:ring-sky-500" checked />
<span class="text-sm">Pro</span>
</label>
</fieldset>
<!-- 7) File upload (basic — drop-zone takes more) -->
<label class="flex items-center gap-2 cursor-pointer">
<span class="px-3 py-1.5 bg-slate-100 rounded text-sm">Choose file</span>
<input type="file" class="sr-only" />
<span class="text-sm text-slate-500">No file chosen</span>
</label>
<!-- 8) Full form layout pattern -->
<form class="space-y-6 max-w-md">
<div class="space-y-1">
<label class="block text-sm font-medium">First name</label>
<input class="block w-full rounded-md border-slate-300 focus:border-sky-500 focus:ring-sky-500" />
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1">
<label class="block text-sm font-medium">City</label>
<input class="block w-full rounded-md border-slate-300 focus:border-sky-500 focus:ring-sky-500" />
</div>
<div class="space-y-1">
<label class="block text-sm font-medium">Postcode</label>
<input class="block w-full rounded-md border-slate-300 focus:border-sky-500 focus:ring-sky-500" />
</div>
</div>
<div class="flex justify-end gap-3 pt-4">
<button type="button" class="px-4 py-2 text-sm rounded border border-slate-300">
Cancel
</button>
<button type="submit" class="px-4 py-2 text-sm rounded bg-sky-600 text-white hover:bg-sky-700">
Save
</button>
</div>
</form>
<!-- 9) Validation states (via aria + classes) -->
<input
type="email"
aria-invalid="true"
class="border-rose-500 focus:border-rose-500 focus:ring-rose-500"
/>
<input
type="email"
aria-invalid="false"
class="border-emerald-500 focus:border-emerald-500 focus:ring-emerald-500"
/>
<!-- Tailwind 3.0+ — :has() and :invalid -->
<input
type="email"
class="valid:border-emerald-500 invalid:border-rose-500 focus:ring-sky-500"
/>
<!-- 10) Disabled state -->
<input disabled class="bg-slate-100 cursor-not-allowed text-slate-500 border-slate-200" />
<button disabled class="bg-slate-300 text-slate-500 cursor-not-allowed px-4 py-2 rounded">
Submit
</button>
<!-- 11) Required indicator -->
<label class="block text-sm font-medium">
Email <span class="text-rose-500">*</span>
</label>
<!-- 12) Loading button -->
<button class="bg-sky-600 text-white px-4 py-2 rounded inline-flex items-center gap-2" disabled>
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
<path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="4" fill="none"/>
</svg>
Saving…
</button>
<!-- 13) Accessibility checklist -->
<!-- • Every input has a <label> (or aria-label) -->
<!-- • Group radio/checkbox with <fieldset> + <legend> -->
<!-- • Use aria-describedby for help / error text -->
<!-- • Use aria-invalid + visible color for errors -->
<!-- • Focus rings — never remove with outline-none unless you replaced them -->
<!-- • Contrast — test with WCAG tools -->
<!-- • Don't disable buttons silently; show loading state -->
<!-- • Use autocomplete attributes (email, current-password, name) -->
<!-- 14) When to reach for headless libraries -->
<!-- Tailwind's @tailwindcss/forms styles native inputs.
For complex widgets (combobox, dropdown, modal), use Headless UI (@headlessui/react),
Radix UI, or React Aria — they handle accessibility + keyboard. Style with Tailwind. -->
<!-- 15) Forms libraries that pair with Tailwind -->
<!-- • React Hook Form — form state -->
<!-- • Zod / Valibot — schema validation -->
<!-- • Headless UI / Radix — accessible primitives -->
<!-- • Conform — server + client validation for React + Remix / Next -->
Why it matters
Install @tailwindcss/forms first — native input styling becomes sane. Pair Tailwind for layout with Headless UI / Radix for complex widgets — you get accessibility AND visual control without reinventing dropdowns.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
<!-- @tailwindcss/forms resets form styles to be Tailwind-friendly --> <input class="rounded border-slate-300 focus:ring-emerald-500">Try it Yourself »
Discussion
Loading…