NEWVersion 1.0 is here.
OpenTailwindv1.0
BlocksTemplatesCollectionsGuidesPricing
Tailwind CSS Forms: How to Style Inputs, Selects and Checkboxes
BlogTailwind CSS Forms: How to Style Inputs, Selects and Checkboxes
GuideOctober 20, 2026·8 min read

Tailwind CSS Forms: How to Style Inputs, Selects and Checkboxes

Style forms with Tailwind CSS v4: inputs, selects, checkboxes, focus and validation states, the @tailwindcss/forms plugin, and accessible newsletter forms.

By OpenTailwind, by lindo.ai

On this page

  1. What changed for forms in v4
  2. Text inputs and textareas
  3. Selects, checkboxes and radios
  4. Using the @tailwindcss/forms plugin
  5. Validation and state variants
  6. A complete newsletter form
  7. FAQ

To style forms with Tailwind CSS, apply utilities directly to each control: a border color, padding, a radius and a focus-visible: ring for inputs, accent-* for native checkboxes and radios, and appearance-none when you want full control over a select. For consistent defaults across every control, add the @tailwindcss/forms plugin with @plugin "@tailwindcss/forms"; in your CSS.

This guide covers both approaches in Tailwind CSS v4, the v4 changes that affect forms, and validation and accessibility patterns that hold up in production.

Table of contents

  • What changed for forms in v4
  • Text inputs and textareas
  • Selects, checkboxes and radios
  • Using the @tailwindcss/forms plugin
  • Validation and state variants
  • A complete newsletter form
  • FAQ

What changed for forms in v4

A few default changes in Tailwind v4 show up most often in forms. Knowing them saves debugging time when upgrading older markup:

  • Border color defaults to currentColor. In v3 a plain border was gray-200. In v4 it uses the text color, so always pair border with a color like border-neutral-300.
  • ring is 1px. The bare ring class used to be 3px. Use ring-2 or ring-3 for visible focus rings.
  • outline-hidden replaces the old outline-none. In v4, outline-hidden hides the outline but keeps it in forced-colors mode, while outline-none sets outline-style: none outright.
  • Placeholder color now defaults to the current text color at 50% opacity rather than a fixed gray.
  • Buttons use cursor: default, matching the browser. Add cursor-pointer if you want the hand cursor.

Text inputs and textareas

A good base input needs a width, padding, a border with a color, a radius, a background for dark mode, and a visible focus state:

<label for="email" class="block text-sm font-medium text-neutral-900 dark:text-white">
  Email
</label>
<input
  id="email"
  type="email"
  autocomplete="email"
  placeholder="[email protected]"
  class="mt-2 block w-full rounded-lg border border-neutral-300 bg-white px-3.5 py-2.5 text-sm
         text-neutral-900 placeholder:text-neutral-400 outline-hidden
         focus-visible:border-indigo-500 focus-visible:ring-3 focus-visible:ring-indigo-500/20
         dark:border-neutral-700 dark:bg-neutral-900 dark:text-white"
/>

Use focus-visible: rather than focus: for buttons and links so mouse users don't see a ring after clicking. For text inputs, browsers treat any focus as visible, so both behave the same; focus-visible: keeps the pattern consistent.

Auto-growing textareas

v4 includes field-sizing-content, which lets a textarea grow with its content without JavaScript. Pair it with a minimum height so it doesn't start as a single line:

<textarea
  rows="3"
  class="block w-full field-sizing-content min-h-24 rounded-lg border border-neutral-300 px-3.5 py-2.5
         dark:border-neutral-700 dark:bg-neutral-900"
></textarea>

Browser support for field-sizing is still incomplete, so treat it as a progressive enhancement. In browsers without it, the textarea simply stays at its rows height.

File inputs

The file: variant styles the button inside a file input: file:mr-4 file:rounded-md file:border-0 file:bg-indigo-50 file:px-3 file:py-2 file:text-indigo-700.

Selects, checkboxes and radios

Native checkboxes with accent color

The simplest approach for checkboxes and radios is to keep the native control and color it with accent-*. It's accessible by default and works in light and dark mode:

<label class="flex items-center gap-3 text-sm">
  <input type="checkbox" class="size-4 accent-indigo-600" />
  Send me product updates
</label>

Wrapping the input in its label makes the whole row clickable and connects the label for screen readers without an id.

Custom selects

Native select arrows look different in every browser. Remove them with appearance-none, then place your own icon in a wrapper using grid stacking so the icon doesn't block clicks:

<div class="grid">
  <select
    class="col-start-1 row-start-1 w-full appearance-none rounded-lg border border-neutral-300 bg-white
           py-2.5 pl-3.5 pr-10 text-sm dark:border-neutral-700 dark:bg-neutral-900"
  >
    <option>Starter</option>
    <option>Team</option>
    <option>Enterprise</option>
  </select>
  <svg class="pointer-events-none col-start-1 row-start-1 mr-3 size-4 self-center justify-self-end text-neutral-500"
       viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
    <path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" />
  </svg>
</div>

Fully custom checkboxes

If you need a design the accent color can't produce, use appearance-none on the checkbox and draw the checked state with checked: utilities, or use a visually hidden input with peer and style a sibling with peer-checked:. Keep the real input in the DOM so keyboard and screen reader behavior still work, and give it a visible focus-visible: ring.

Using the @tailwindcss/forms plugin

The official forms plugin applies a reset to form controls so they look consistent across browsers and are easy to restyle with utilities. Install it and register it in CSS:

npm install -D @tailwindcss/forms

/* app.css */
@import "tailwindcss";
@plugin "@tailwindcss/forms";

By default the plugin styles elements globally by type, such as input[type="text"], select and textarea. If you only want its styles on elements you opt in, switch to the class strategy:

@plugin "@tailwindcss/forms" {
  strategy: "class";
}

Then add form-input, form-select, form-textarea, form-checkbox or form-radio to each control. The class strategy is the safer choice when adding the plugin to an existing project or when mixing in third-party widgets. With the plugin active, checkboxes accept text color utilities, so text-indigo-600 sets the checked color.

Call to action block with an image, feature list and signup form
Call to action block with an image, feature list and signup form — View & copy code

Validation and state variants

Tailwind has variants for every form state the browser exposes, so most validation styling needs no JavaScript:

  • invalid: and valid: match the :invalid and :valid pseudo-classes.
  • user-invalid: and user-valid: only apply after the user has interacted with the field, which avoids showing errors on an untouched form.
  • required:, disabled:, read-only:, placeholder-shown: and autofill: cover the other common states.
  • peer-* and group-has-* let siblings and parents react to a field's state.
<div>
  <input
    type="email"
    required
    class="peer block w-full rounded-lg border border-neutral-300 px-3.5 py-2.5
           user-invalid:border-red-500 user-invalid:ring-3 user-invalid:ring-red-500/20"
  />
  <p class="mt-2 hidden text-sm text-red-600 peer-user-invalid:block">
    Enter a valid email address.
  </p>
</div>

For server-side errors, set aria-invalid="true" on the field and style it with the aria-invalid: variant. Connect the error message with aria-describedby so screen readers announce it.

Disabled controls need two signals: reduced opacity and a non-interactive cursor, for example disabled:cursor-not-allowed disabled:opacity-50. The has-disabled: variant on a wrapping label can dim the label text too.

A complete newsletter form

Putting it together, here's a single-row signup form that stacks on mobile, has a visually hidden label, and styles focus and disabled states:

<form class="flex flex-col gap-3 sm:flex-row" action="/subscribe" method="post">
  <label for="newsletter-email" class="sr-only">Email address</label>
  <input
    id="newsletter-email"
    name="email"
    type="email"
    required
    autocomplete="email"
    placeholder="Enter your email"
    class="min-w-0 flex-1 rounded-lg border border-neutral-300 bg-white px-4 py-3 text-sm outline-hidden
           focus-visible:border-indigo-500 focus-visible:ring-3 focus-visible:ring-indigo-500/20
           dark:border-neutral-700 dark:bg-neutral-900 dark:text-white"
  />
  <button
    type="submit"
    class="cursor-pointer rounded-lg bg-indigo-600 px-5 py-3 text-sm font-medium text-white
           hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600
           disabled:cursor-not-allowed disabled:opacity-50"
  >
    Subscribe
  </button>
</form>

The min-w-0 on the input lets it shrink inside the flex row instead of pushing the button off-screen on narrow devices.

Newsletter card with an inline email input and subscribe button
Newsletter card with an inline email input and subscribe button — View & copy code
Footer with contact details and a newsletter signup form
Footer with contact details and a newsletter signup form — View & copy code

More form layouts are in the CTA blocks and footer blocks, including subscribe cards and contact forms in light and dark mode. If you'd rather not hand-code it, Lindo.ai can build a site from any OpenTailwind template with AI.

FAQ

Do I need the @tailwindcss/forms plugin?

No. You can style every control with plain utilities, as shown above. The plugin is useful when you have many forms and want consistent cross-browser defaults without repeating base classes everywhere.

Why is my input border black after upgrading to v4?

In v4, border uses currentColor by default instead of gray-200. Add an explicit color such as border-neutral-300, or set a global default in your base layer.

How do I remove the blue focus outline but stay accessible?

Replace it rather than removing it. Use outline-hidden together with a focus-visible:ring-* or focus-visible:outline-* style that has enough contrast against the background.

How do I show errors only after the user types?

Use the user-invalid: variant. It matches :user-invalid, which only applies after the user has changed the field or tried to submit the form.

Tailwind CSS v4 gives you everything needed to build forms without writing custom CSS: base utilities for inputs, accent-* and appearance-none for native controls, state variants like user-invalid: and disabled:, and an optional plugin for consistent defaults. Set explicit border colors, use visible focus rings, keep labels connected to their fields, and your forms will look consistent and work for keyboard and screen reader users.

Library access: Tutorials and their educational demos are public. Copying library designs requires an account: 100 selected blocks and five templates per industry are free. Pro lifetime access unlocks the complete library.

Browse 1,761 Tailwind CSS blocks

100 free blocks, five free templates per industry, and Pro lifetime access to the complete block and template library.

Browse BlocksTemplates
← Previous article

Tailwind CSS Footer Examples: 8 Free Layouts with Code

Next article →

Tailwind CSS Grid: A Practical Layout Guide for v4

OpenTailwindv1.0

A little less starting from scratch.
A lot more making it yours.

Made with care by lindo.ai

The library

All blocksAll templatesHero sectionsPricing sections

Around here

Our storyGuides & ideasDesign examplesMembership & pricingQuestions?

Good design is a
good place to start.

FREE TO EXPLORE. YOURS TO BUILD.
© 2026 OpenTailwind · by lindo.ai
TermsPrivacyCookies All creativity welcome