Tailwind CSS 30 🎨 Pseudo-Class Variants (hover:, focus:, active:, focus-within:, focus-visible:)
A pseudo-class is a selector that matches an element based on its state or its relationship to other elements. The :hover pseudo-class matches when the pointer is over the element. The :focus pseudo-class matches when the element has keyboard focus. Tailwind exposes these as variants: a prefix that applies a utility only when the state is active. The variant system is what makes Tailwind interactive. Without it, every hover effect would require custom CSS. With it, hover:bg-blue-600 is a complete specification.
This chapter covers the pseudo-class variants in full. You will learn the interaction states—hover, focus, active, visited—the focus family that includes focus-within and focus-visible, the form states like disabled and checked, and the structural pseudo-classes like first, last, odd, and even. You will also see how variants stack, how they combine with group and peer modifiers, and the accessibility requirements that make focus-visible preferable to focus for most interactions.
Key point: Pseudo-class variants apply a utility only when the element matches the pseudo-class. hover:bg-blue-600 applies the background color on hover and removes it when the pointer leaves. Variants stack left to right: md:hover:bg-blue-600 applies on medium screens and up, when hovered. The order matters for readability but not for correctness in most cases.
Why pseudo-class variants exist
The state problem. An element is not always in the same state. A button is idle, hovered, focused, active, or disabled. Each state may need a different appearance. Writing the CSS for each state manually is repetitive: .button:hover { ... }, .button:focus { ... }, .button:active { ... }. Tailwind’s variants move the state into the utility name, so the state and the style are declared together.
The accessibility problem. Hover effects are easy. Focus effects are mandatory. Keyboard users navigate with Tab, and the focused element must be visible. The default browser focus outline is inconsistent across browsers and often removed by developers who find it ugly. focus-visible provides a way to show a focus indicator only when the user is navigating with the keyboard, not when they click with a mouse. This preserves the accessibility requirement without showing the outline on every click.
The specificity problem. Pseudo-class selectors have higher specificity than plain class selectors. .button:hover beats .button. This can cause conflicts when utilities are combined. Tailwind’s variant system generates selectors with consistent specificity, and the order in the generated stylesheet determines which utility wins when two have the same specificity.
The composition problem. Variants stack. md:focus:ring-2 applies the ring on medium screens and up, when the element is focused. dark:hover:bg-gray-800 applies the dark background on hover in dark mode. group-hover:opacity-100 applies the opacity when the parent group is hovered. The variant system composes these into a single selector.
The form-state problem. Form elements have states that are not pointer-driven: disabled, checked, required, valid, invalid, placeholder-shown. These states are represented by pseudo-classes, and Tailwind provides variants for each. disabled:opacity-50 dims a disabled input. checked:bg-blue-600 colors a checked checkbox. The state is part of the element, and the variant targets it.
a. Interaction states: hover, focus, active, visited
The four interaction states cover the lifecycle of a pointer or keyboard interaction.
hover: applies when the pointer is over the element. It is the most common variant.
<button class="bg-blue-500 hover:bg-blue-600">
Hover me
</button>
The background is blue by default and darker blue on hover. The hover: variant only applies on devices that support hover. On touch devices, the hover state may not trigger, which is why hover effects should never be the only way to access functionality.
focus: applies when the element has focus. An element receives focus when it is clicked or when it is reached with the Tab key.
<input class="border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200" />
The input has a gray border by default. On focus, the border changes to blue and a ring appears. This is the standard focus indicator.
active: applies when the element is being activated—while the mouse button is held down or the key is pressed.
<button class="bg-blue-500 active:bg-blue-700">
Press me
</button>
The active state is brief. It provides feedback that the click is registering.
visited: applies to links that have been visited. It is used less often because of privacy concerns and because most designs do not distinguish visited links.
<a href="/page" class="text-blue-600 visited:text-purple-600">
Link
</a>
The browser restricts which properties can be styled with :visited for privacy reasons. Only color-related properties are allowed.
These four states can be combined. hover:focus:bg-blue-600 applies when the element is both hovered and focused. In practice, this is rare because hover and focus often coincide on desktop.
b. The focus family: focus, focus-within, focus-visible
The :focus pseudo-class has two relatives that solve specific problems.
focus-within: applies when the element itself or any of its descendants has focus. This is used for containers that should show a focus indicator when any child is focused.
<div class="border-gray-300 focus-within:border-blue-500 focus-within:ring-2">
<label>Email</label>
<input type="email" class="outline-none" />
</div>
The container shows the border and ring when the input inside it is focused. The input itself has no visible outline. This is a common pattern for form fields where the label and input are wrapped in a styled container.
focus-visible: applies when the element has focus and the browser determines that the focus should be visible. The heuristic is browser-dependent, but the general rule is that :focus-visible matches when the user is navigating with the keyboard, and does not match when the user clicks with the mouse.
<button class="focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500">
Save
</button>
The button removes the default outline on focus and adds a custom ring only when the focus is visible. A mouse click focuses the button without showing the ring. A Tab keypress focuses the button and shows the ring. This is the recommended pattern for accessible focus indicators.
The difference between focus: and focus-visible: matters for usability. If focus: is used for every interactive element, clicking a button shows a focus ring, which many users find visually noisy. focus-visible: shows the ring only when it is needed—for keyboard navigation. The focus-visible: variant is the modern replacement for the “remove the outline and add a custom one” pattern.
For form inputs, focus: is still appropriate because the focus indicator on an input is expected and useful regardless of how focus was acquired. For buttons, links, and other interactive elements that are not text fields, focus-visible: is usually the better choice.
c. Form states and structural pseudo-classes
Form elements have states that are not pointer-driven. Tailwind provides variants for the common ones.
| Variant | Matches When |
|---|---|
disabled: | Element is disabled |
enabled: | Element is not disabled |
checked: | Checkbox or radio is checked |
indeterminate: | Checkbox is in indeterminate state |
required: | Input is required |
optional: | Input is not required |
valid: | Input passes validation |
invalid: | Input fails validation |
in-range: | Input value is in range |
out-of-range: | Input value is out of range |
placeholder-shown: | Placeholder is visible (input is empty) |
autofill: | Browser autofilled the input |
<input class="border-gray-300 disabled:bg-gray-100 disabled:text-gray-500" disabled />
<input type="checkbox" class="checked:bg-blue-600" checked />
<input class="border-gray-300 invalid:border-red-500 valid:border-green-500" />
<input class="placeholder-shown:italic" placeholder="Type here" />
The disabled: variant is used to dim disabled controls. The checked: variant is used to style checked checkboxes and radios. The valid: and invalid: variants are used for validation feedback. The placeholder-shown: variant is used to style the input differently when it is empty and showing its placeholder.
The structural pseudo-classes match elements based on their position among siblings.
| Variant | Matches |
|---|---|
first: | First child |
last: | Last child |
only: | Only child |
odd: | Odd-numbered children |
even: | Even-numbered children |
first-of-type: | First of its type |
last-of-type: | Last of its type |
only-of-type: | Only of its type |
empty: | No children |
<ul>
<li class="odd:bg-gray-50 even:bg-white">Item</li>
<li class="odd:bg-gray-50 even:bg-white">Item</li>
<li class="odd:bg-gray-50 even:bg-white">Item</li>
</ul>
The odd: and even: variants create striped rows without JavaScript or custom CSS. The first: and last: variants remove borders or margins from the first or last element in a list.
<li class="border-b last:border-b-0">Item</li>
The last:border-b-0 removes the bottom border from the last item in a list, which is a common pattern for lists where each item has a border except the last.
The not-* variant negates a pseudo-class. not-first:mt-4 applies margin-top to every element except the first. In Tailwind v4, the syntax is not-first: or the older [&:not(:first-child)]: arbitrary variant.
Complete Example Session
<!-- ============================================ -->
<!-- PART 1: HOVER -->
<!-- ============================================ -->
<button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
Hover me
</button>
<!-- ============================================ -->
<!-- PART 2: FOCUS -->
<!-- ============================================ -->
<input class="border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 rounded px-3 py-2" />
<!-- ============================================ -->
<!-- PART 3: ACTIVE -->
<!-- ============================================ -->
<button class="bg-blue-500 active:bg-blue-700 text-white px-4 py-2 rounded">
Press me
</button>
<!-- ============================================ -->
<!-- PART 4: VISITED -->
<!-- ============================================ -->
<a href="/page" class="text-blue-600 visited:text-purple-600">Link</a>
<!-- ============================================ -->
<!-- PART 5: FOCUS-WITHIN -->
<!-- ============================================ -->
<div class="border border-gray-300 rounded focus-within:border-blue-500 focus-within:ring-2">
<label class="block text-sm">Email</label>
<input type="email" class="w-full outline-none" />
</div>
<!-- ============================================ -->
<!-- PART 6: FOCUS-VISIBLE -->
<!-- ============================================ -->
<button class="focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 px-4 py-2">
Save
</button>
<!-- ============================================ -->
<!-- PART 7: DISABLED AND CHECKED -->
<!-- ============================================ -->
<input class="border border-gray-300 disabled:bg-gray-100 disabled:text-gray-500 rounded px-3 py-2" disabled />
<input type="checkbox" class="checked:bg-blue-600 h-5 w-5 rounded" />
<!-- ============================================ -->
<!-- PART 8: VALID AND INVALID -->
<!-- ============================================ -->
<input class="border border-gray-300 invalid:border-red-500 valid:border-green-500 rounded px-3 py-2" />
<!-- ============================================ -->
<!-- PART 9: STRUCTURAL PSEUDO-CLASSES -->
<!-- ============================================ -->
<ul>
<li class="odd:bg-gray-50 even:bg-white px-4 py-2">Item 1</li>
<li class="odd:bg-gray-50 even:bg-white px-4 py-2">Item 2</li>
<li class="odd:bg-gray-50 even:bg-white px-4 py-2">Item 3</li>
</ul>
<!-- ============================================ -->
<!-- PART 10: STACKED VARIANTS -->
<!-- ============================================ -->
<button class="bg-blue-500 md:hover:bg-blue-600 focus-visible:ring-2 dark:bg-blue-700 dark:hover:bg-blue-600 px-4 py-2">
Stacked
</button>
The ten parts covered hover, focus, active, visited, focus-within, focus-visible, disabled and checked, valid and invalid, structural pseudo-classes, and stacked variants.
Quick Reference
Interaction States
| Variant | Matches |
|---|---|
hover: | Pointer over element |
focus: | Element has focus |
active: | Element being activated |
visited: | Link has been visited |
Focus Family
| Variant | Matches |
|---|---|
focus: | Element has focus |
focus-within: | Element or descendant has focus |
focus-visible: | Focus is visible (keyboard) |
Form States
| Variant | Matches |
|---|---|
disabled: | Element disabled |
enabled: | Element enabled |
checked: | Checkbox/radio checked |
indeterminate: | Checkbox indeterminate |
required: | Input required |
optional: | Input optional |
valid: | Input passes validation |
invalid: | Input fails validation |
placeholder-shown: | Input is empty |
autofill: | Browser autofilled |
Structural Pseudo-Classes
| Variant | Matches |
|---|---|
first: | First child |
last: | Last child |
only: | Only child |
odd: | Odd children |
even: | Even children |
first-of-type: | First of its type |
last-of-type: | Last of its type |
empty: | No children |
Stacking Order
| Example | Meaning |
|---|---|
md:hover:bg-blue-600 | Medium screens, hovered |
dark:focus:ring-2 | Dark mode, focused |
group-hover:opacity-100 | Parent hovered |
not-first:mt-4 | Not the first child |
Best Practices
✅ Do This:
<!-- Use hover for pointer feedback -->
<button class="hover:bg-blue-600">Hover</button> <!-- ✅ -->
<!-- Use focus-visible for keyboard focus -->
<button class="focus-visible:ring-2">Focus</button> <!-- ✅ -->
<!-- Use focus-within for wrapped inputs -->
<div class="focus-within:ring-2">
<input class="outline-none" />
</div> <!-- ✅ -->
<!-- Use active for click feedback -->
<button class="active:scale-95">Press</button> <!-- ✅ -->
<!-- Use disabled for disabled controls -->
<input class="disabled:opacity-50" disabled /> <!-- ✅ -->
<!-- Use odd/even for striped rows -->
<li class="odd:bg-gray-50 even:bg-white">Item</li> <!-- ✅ -->
<!-- Use last:border-b-0 to remove the last border -->
<li class="border-b last:border-b-0">Item</li> <!-- ✅ -->
<!-- Stack variants for specific combinations -->
<button class="md:hover:bg-blue-600 dark:focus:ring-2"> <!-- ✅ -->
❌ Don’t Do This:
<!-- Don't remove focus outline without a replacement -->
<button class="focus:outline-none">No visible focus</button> <!-- ❌ -->
<!-- Don't use focus for buttons when focus-visible fits -->
<button class="focus:ring-2">Ring on every click</button> <!-- ⚠️ -->
<!-- Don't rely on hover for touch devices -->
<button class="hover:bg-blue-600">Hover only</button> <!-- ⚠️ -->
<!-- Don't use visited for security-relevant styling -->
<a href="/admin" class="visited:text-red-600"> <!-- ⚠️ -->
<!-- Don't stack too many variants -->
<button class="md:dark:hover:focus:active:bg-blue-600"> <!-- ⚠️ -->
<!-- Don't forget motion-reduce for transform variants -->
<button class="active:scale-95"> <!-- ⚠️ -->
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Focus outline removed | focus:outline-none without replacement | Add focus-visible:ring-2 |
| Hover on touch devices | No hover capability | Provide an alternative |
| Ring shows on click | Used focus: | Use focus-visible: |
focus-within not working | No focusable child | Add tabindex or a focusable element |
checked not applying | Element not a checkbox | Use on checkbox/radio only |
| Odd/even reversed | Zero-indexed confusion | odd is 1st, 3rd, 5th |
| Stacked variant order | Misunderstanding evaluation | Variants apply left to right |
Real-World Examples
1. Button with Hover and Active
<button class="bg-blue-500 hover:bg-blue-600 active:bg-blue-700">Save</button>
2. Input with Focus Ring
<input class="border focus:border-blue-500 focus:ring-2 focus:ring-blue-200" />
3. Accessible Focus
<button class="focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500">Submit</button>
4. Form Field Container
<div class="border focus-within:border-blue-500 focus-within:ring-2">
<label>Email</label>
<input class="outline-none" />
</div>
5. Disabled Button
<button class="disabled:opacity-50 disabled:cursor-not-allowed" disabled>Disabled</button>
6. Checked Checkbox
<input type="checkbox" class="checked:bg-blue-600 checked:border-blue-600" />
7. Invalid Input
<input class="border invalid:border-red-500 invalid:text-red-600" />
8. Striped Table
<tr class="odd:bg-white even:bg-gray-50">...</tr>
9. List Without Last Border
<li class="border-b last:border-b-0">Item</li>
10. Stacked Variants
<button class="bg-blue-500 md:hover:bg-blue-600 dark:bg-blue-700 dark:hover:bg-blue-600">
Responsive and dark mode
</button>
Visual
Variant Application
┌─────────────────────────────────────────────────────────────┐
│ DEFAULT STATE │
│ <button class="bg-blue-500 hover:bg-blue-600"> │
│ │
│ bg-blue-500 │
│ ┌─────────────┐ │
│ │ Button │ Blue background │
│ └─────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ HOVER STATE │
│ │
│ hover:bg-blue-600 │
│ ┌─────────────┐ │
│ │ Button │ Darker blue background │
│ └─────────────┘ │
│ │
│ The hover utility overrides the default. │
│ │
└─────────────────────────────────────────────────────────────┘
Focus vs Focus-Visible
┌─────────────────────────────────────────────────────────────┐
│ focus:ring-2 │
│ │
│ Mouse click: ring shows │
│ Tab keypress: ring shows │
│ Programmatic: ring shows │
│ │
│ Ring shows for all focus events. │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ focus-visible:ring-2 │
│ │
│ Mouse click: ring does NOT show │
│ Tab keypress: ring shows │
│ Programmatic: depends on browser heuristic │
│ │
│ Ring shows only when focus is visible. │
│ │
│ Better for buttons and links. │
│ │
└─────────────────────────────────────────────────────────────┘
Focus-Within
┌─────────────────────────────────────────────────────────────┐
│ WITHOUT focus-within │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Label │ │
│ │ ┌───────────────────────────┐ │ │
│ │ │ Input (focused) │ │ Input has ring │
│ │ └───────────────────────────┘ │ Container unchanged │
│ └─────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ WITH focus-within │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Label │ │
│ │ ┌───────────────────────────┐ │ │
│ │ │ Input (focused) │ │ │
│ │ └───────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
│ Container shows ring when input is focused. │
│ │
│ The container responds to the child's focus. │
│ │
└─────────────────────────────────────────────────────────────┘
Structural Pseudo-Classes
┌─────────────────────────────────────────────────────────────┐
│ <ul> │
│ <li class="odd:bg-gray-50 even:bg-white">Item 1</li> │
│ <li class="odd:bg-gray-50 even:bg-white">Item 2</li> │
│ <li class="odd:bg-gray-50 even:bg-white">Item 3</li> │
│ <li class="odd:bg-gray-50 even:bg-white">Item 4</li> │
│ </ul> │
│ │
│ Item 1: odd → bg-gray-50 │
│ Item 2: even → bg-white │
│ Item 3: odd → bg-gray-50 │
│ Item 4: even → bg-white │
│ │
│ Striped rows without JavaScript. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Variant | Purpose |
|---|---|
hover: | Pointer over element |
focus: | Element has focus |
active: | Element being activated |
visited: | Link has been visited |
focus-within: | Element or descendant has focus |
focus-visible: | Focus is visible (keyboard) |
disabled: | Element disabled |
checked: | Checkbox/radio checked |
invalid: | Input fails validation |
placeholder-shown: | Input is empty |
first: | First child |
last: | Last child |
odd: | Odd children |
even: | Even children |
Key takeaways:
- Pseudo-class variants apply a utility only when the element matches the state.
hover:bg-blue-600applies the background on hover and removes it when the pointer leaves. The variant is a prefix, and the utility is the style. focus-visibleis preferable tofocusfor buttons and links.focus:shows the indicator for all focus events, including mouse clicks.focus-visible:shows it only when the browser determines that the focus should be visible, which is typically keyboard navigation. For form inputs,focus:is still appropriate.focus-withinapplies when the element or any descendant has focus. It is used for containers that wrap form inputs and should show a focus indicator when the input inside is focused.- The focus indicator must not be removed without a replacement. Removing the outline and providing no alternative makes the element unusable for keyboard navigation.
focus-visible:ring-2is the standard replacement. - Form states have their own variants.
disabled:,checked:,invalid:,valid:,required:, andplaceholder-shown:target the form element’s state without JavaScript. - Structural pseudo-classes match position.
first:,last:,odd:,even:,only:, and the-of-typevariants match elements based on their position among siblings. They are used for striped lists, border removal, and layout adjustments. - Variants stack.
md:hover:bg-blue-600applies the background on medium screens and up, when hovered.dark:focus:ring-2applies the ring in dark mode, when focused. The variants compose, and the utility applies only when all the conditions are met. - Hover effects should never be the only way to access functionality. On touch devices, hover does not exist. Any functionality that is only available on hover is inaccessible to touch users. Provide an alternative, such as a click or a focus state.
Remember: Pseudo-class variants are how Tailwind handles element state. They move the state into the utility name, so hover:bg-blue-600 is a complete specification for “blue on hover.” The interaction states—hover, focus, active—cover pointer and keyboard interaction. The focus family—focus, focus-within, focus-visible—covers the different ways an element can be focused. The form states—disabled, checked, invalid—cover the states of form elements. The structural pseudo-classes—first, last, odd, even—cover position. And they all stack with each other and with breakpoints, dark mode, and the group and peer modifiers. Use focus-visible for buttons and links, never remove the focus indicator without a replacement, and never rely on hover as the only way to access functionality.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!