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>
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
What to read next
- Dark mode — switch color schemes with the
dark:variant - Animations — add motion with
transition/animate-* - Tailwind CSS Guide table of contents — back to the structure of all 18 chapters