| |

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.

VariantMatches 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.

VariantMatches
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

VariantMatches
hover:Pointer over element
focus:Element has focus
active:Element being activated
visited:Link has been visited

Focus Family

VariantMatches
focus:Element has focus
focus-within:Element or descendant has focus
focus-visible:Focus is visible (keyboard)

Form States

VariantMatches
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

VariantMatches
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

ExampleMeaning
md:hover:bg-blue-600Medium screens, hovered
dark:focus:ring-2Dark mode, focused
group-hover:opacity-100Parent hovered
not-first:mt-4Not 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

PitfallWhy It HappensFix
Focus outline removedfocus:outline-none without replacementAdd focus-visible:ring-2
Hover on touch devicesNo hover capabilityProvide an alternative
Ring shows on clickUsed focus:Use focus-visible:
focus-within not workingNo focusable childAdd tabindex or a focusable element
checked not applyingElement not a checkboxUse on checkbox/radio only
Odd/even reversedZero-indexed confusionodd is 1st, 3rd, 5th
Stacked variant orderMisunderstanding evaluationVariants 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

VariantPurpose
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-600 applies the background on hover and removes it when the pointer leaves. The variant is a prefix, and the utility is the style.
  • focus-visible is preferable to focus for 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-within applies 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-2 is the standard replacement.
  • Form states have their own variants. disabled:, checked:, invalid:, valid:, required:, and placeholder-shown: target the form element’s state without JavaScript.
  • Structural pseudo-classes match position. first:, last:, odd:, even:, only:, and the -of-type variants 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-600 applies the background on medium screens and up, when hovered. dark:focus:ring-2 applies 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!