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

Styling 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