Skip to main content

State Variants

In this chapter, you learn how to style elements based on their state with Tailwind CSS.

Hover, focus, active​

hover​

Specify styles for when the mouse hovers.

<button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-sm">
Color changes on hover
</button>

<a href="#" class="text-blue-500 hover:text-blue-700 hover:underline">
Underline on hover
</a>
info

In v4, hover styles are wrapped in @media (hover: hover) and are not applied on devices that do not support hover (touch-only). If you rely on tap-triggered behavior as in v3 and earlier, you can restore the old behavior with @custom-variant hover (&:hover);.

focus​

Specify styles for when an element is focused.

<input
type="text"
class="border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-hidden
px-4 py-2 rounded-sm"
placeholder="The border turns blue on focus"
/>

<button class="bg-blue-500 focus:ring-4 focus:ring-blue-300 text-white px-4 py-2 rounded-sm">
Focus ring
</button>

focus-visible​

Applied only to focus from keyboard navigation.

<button class="focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:outline-hidden
px-4 py-2 rounded-sm">
Show the ring only on keyboard focus
</button>

focus-within​

Applied to the parent element when a child element is focused.

<div class="border border-gray-300 focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500 p-4 rounded-sm">
<label class="block text-sm mb-1">Label</label>
<input type="text" class="w-full focus:outline-hidden" />
</div>

active​

Specify styles for while clicking.

<button class="bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white px-4 py-2 rounded-sm">
Color changes while clicking
</button>

Form states​

disabled​

<button class="bg-blue-500 disabled:bg-gray-300 disabled:cursor-not-allowed text-white px-4 py-2 rounded-sm" disabled>
Disabled button
</button>

<input
type="text"
class="border disabled:bg-gray-100 disabled:text-gray-500 disabled:cursor-not-allowed px-4 py-2 rounded-sm"
disabled
value="Disabled input"
/>

required​

<input
type="text"
class="border required:border-red-500 px-4 py-2 rounded-sm"
required
/>

invalid​

<input
type="email"
class="border invalid:border-red-500 invalid:text-red-500 focus:invalid:ring-red-500 px-4 py-2 rounded-sm"
value="invalid-email"
/>

valid​

<input
type="email"
class="border valid:border-green-500 px-4 py-2 rounded-sm"
value="valid@email.com"
/>

checked​

<label class="flex items-center gap-2">
<input
type="checkbox"
class="w-5 h-5 appearance-none rounded-sm border border-gray-300 checked:bg-blue-500 checked:border-transparent"
/>
Checkbox
</label>

indeterminate​

<input
type="checkbox"
id="some-checkbox"
class="appearance-none indeterminate:bg-gray-300"
/>

<script>
document.getElementById('some-checkbox').indeterminate = true;
</script>

The indeterminate state cannot be set via an HTML attribute; it can only be set from JavaScript.

placeholder-shown​

<input
type="text"
class="border placeholder-shown:border-gray-300 not-placeholder-shown:border-blue-500 px-4 py-2 rounded-sm"
placeholder="Placeholder"
/>

read-only​

<input
type="text"
class="border read-only:bg-gray-100 read-only:cursor-not-allowed px-4 py-2 rounded-sm"
readonly
value="Read-only"
/>

Groups​

Style child elements based on the state of a parent element.

group-hover​

<div class="group p-4 bg-white rounded-lg shadow-sm hover:bg-blue-500 cursor-pointer">
<h3 class="font-bold group-hover:text-white">Title</h3>
<p class="text-gray-600 group-hover:text-blue-100">Description</p>
</div>

group-focus​

<button class="group px-4 py-2 rounded-sm border focus:ring-2">
<span class="group-focus:text-blue-500">Turns blue on focus</span>
</button>

Named groups​

You can distinguish between multiple groups.

<div class="group/card p-4 bg-white rounded-lg shadow-sm hover:shadow-lg">
<div class="group/title">
<h3 class="font-bold group-hover/card:text-blue-500">Title</h3>
<button class="opacity-0 group-hover/title:opacity-100">Edit</button>
</div>
<p class="group-hover/card:text-gray-700">Description</p>
</div>

Peers (sibling elements)​

Style based on the state of a sibling element.

peer​

<div>
<input type="checkbox" id="toggle" class="peer sr-only" />
<label for="toggle" class="cursor-pointer">Toggle</label>
<div class="hidden peer-checked:block mt-2 p-4 bg-gray-100 rounded-sm">
Content shown when checked
</div>
</div>

Form validation​

<div>
<input
type="email"
class="peer border px-4 py-2 rounded-sm invalid:border-red-500"
placeholder="Email address"
required
/>
<p class="hidden peer-invalid:block text-red-500 text-sm mt-1">
Please enter a valid email address
</p>
</div>

Named peers​

<div>
<input type="text" class="peer/name" placeholder="Name" />
<input type="email" class="peer/email" placeholder="Email" />
<p class="hidden peer-invalid/email:block text-red-500">
Email is invalid
</p>
</div>

Structure-based pseudo-classes​

first / last​

<ul>
<li class="py-2 border-b first:pt-0 last:pb-0 last:border-b-0">Item 1</li>
<li class="py-2 border-b first:pt-0 last:pb-0 last:border-b-0">Item 2</li>
<li class="py-2 border-b first:pt-0 last:pb-0 last:border-b-0">Item 3</li>
</ul>

odd / even​

<table>
<tbody>
<tr class="odd:bg-gray-50 even:bg-white">
<td class="p-4">Row 1</td>
</tr>
<tr class="odd:bg-gray-50 even:bg-white">
<td class="p-4">Row 2</td>
</tr>
<tr class="odd:bg-gray-50 even:bg-white">
<td class="p-4">Row 3</td>
</tr>
</tbody>
</table>

first-of-type / last-of-type​

<div>
<p class="first-of-type:font-bold">First p element</p>
<p>Second p element</p>
<p class="last-of-type:text-gray-500">Last p element</p>
</div>

only-child​

<ul>
<li class="only:text-center">Centered only when it is the only child</li>
</ul>

empty​

<div class="p-4 empty:hidden">
<!-- Hidden when empty -->
</div>

before / after​

You style the ::before and ::after pseudo-elements.

<span class="before:content-['*'] before:text-red-500 before:mr-1">
Required field
</span>

<a href="#" class="relative after:absolute after:bottom-0 after:left-0 after:w-full after:h-0.5 after:bg-blue-500 after:scale-x-0 hover:after:scale-x-100 after:transition-transform">
Underline animation on hover
</a>

Pseudo-elements without content​

<div class="relative before:content-[''] before:block before:absolute before:inset-0 before:bg-linear-to-r before:from-blue-500 before:to-purple-500 before:opacity-50">
Gradient overlay
</div>

has​

The has-* variant, added in v3.4. You can use the CSS :has() selector as a Tailwind utility. In v4, named forms such as has-checked: are also available.

<!-- When a child element has a checked input -->
<div class="has-[:checked]:bg-blue-50 has-[:checked]:border-blue-500 border p-4 rounded-sm">
<label class="flex items-center gap-2">
<input type="checkbox" />
Option
</label>
</div>

<!-- When it has a specific child element -->
<div class="has-[img]:p-0 p-4">
<img src="..." />
</div>

Combining variants​

You can combine multiple variants.

<!-- Responsive + hover -->
<button class="bg-blue-500 md:hover:bg-blue-600 px-4 py-2 rounded-sm">
Hover effect only at md and above
</button>

<!-- Group + focus -->
<div class="group">
<button class="group-focus:ring-2">Button</button>
</div>

<!-- Dark mode + hover -->
<button class="bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700">
Dark-mode-aware hover
</button>

Practical examples​

Interactive card​

<div class="group relative bg-white rounded-xl shadow-md hover:shadow-xl transition-shadow overflow-hidden">
<div class="absolute inset-0 bg-linear-to-r from-blue-500 to-purple-500 opacity-0 group-hover:opacity-10 transition-opacity"></div>
<img src="..." class="w-full h-48 object-cover group-hover:scale-105 transition-transform duration-300" />
<div class="p-6">
<h3 class="font-bold text-xl group-hover:text-blue-500 transition-colors">Title</h3>
<p class="mt-2 text-gray-600">Description...</p>
<button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded-sm opacity-0 group-hover:opacity-100 translate-y-2 group-hover:translate-y-0 transition-all">
See details
</button>
</div>
</div>

Floating-label input​

<div class="relative">
<input
type="text"
id="email"
class="peer w-full px-4 py-3 border rounded-lg placeholder-transparent focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Email address"
/>
<label
for="email"
class="absolute left-4 top-3 text-gray-500 transition-all
peer-placeholder-shown:top-3 peer-placeholder-shown:text-base
peer-focus:-top-2.5 peer-focus:text-sm peer-focus:text-blue-500
-top-2.5 text-sm bg-white px-1"
>
Email address
</label>
</div>

Accordion​

<div class="border rounded-lg">
<div>
<input type="checkbox" id="accordion1" class="peer sr-only" />
<label for="accordion1" class="flex justify-between items-center p-4 cursor-pointer hover:bg-gray-50 peer-checked:[&_svg]:rotate-180">
<span class="font-medium">Section 1</span>
<svg class="w-5 h-5 transition-transform">...</svg>
</label>
<div class="hidden peer-checked:block px-4 pb-4">
Accordion content...
</div>
</div>
</div>

Summary​

  • Use hover: / focus: / active: for basic interactions
  • Use disabled: / invalid: / checked: for form states
  • Use group-* to style based on the state of a parent element
  • Use peer-* to style based on the state of a sibling element
  • Use first: / last: / odd: / even: for position-based styling
  • Use before: / after: for pseudo-elements
  • Use has-* to style based on the state of child elements
  • Variants can be combined