Tailwind Hover Focus and Active States
Websites feel lifeless without interactive feedback. When a user hovers over a button, clicks a link, or tabs into a form field, the element should visually respond. Tailwind makes this easy with state-based prefix classes.
What Are State Modifiers?
A state modifier is a prefix you add before a utility class. It tells the browser: "apply this style only when the user is doing something specific."
Normal class: bg-blue-500
Hover version: hover:bg-blue-700
Normal: text-gray-700
Focus: focus:text-blackThe pattern is always state:utility-class — the colon connects the state to the class.
The Hover State
Hover styles apply when a user moves the cursor over an element. Think of it as a preview reaction — the user has not committed to clicking yet.
Basic Hover Examples
<!-- Change background on hover -->
<button class="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2 rounded">
Click Me
</button>
<!-- Change text color on hover -->
<a class="text-gray-700 hover:text-blue-600" href="#">Read More</a>
<!-- Show underline on hover -->
<a class="no-underline hover:underline" href="#">Link</a>
<!-- Change opacity on hover -->
<img class="opacity-100 hover:opacity-75" src="photo.jpg">Hover State Flow Diagram
Mouse away from button:
[ bg-blue-500 button ]
Mouse moves over button:
[ bg-blue-700 button ] ← darker
Mouse moves away:
[ bg-blue-500 button ] ← returns to normalHover with Scale and Shadow
Cards and thumbnails often grow slightly on hover to signal they are clickable.
<div class="shadow hover:shadow-lg hover:scale-105 transition-all duration-200 cursor-pointer rounded-lg p-4">
Hover over this card
</div>The Focus State
Focus activates when an element receives keyboard focus — either by pressing Tab or clicking into it. Focus styles are critical for accessibility. Users who navigate with a keyboard rely on visible focus indicators to know where they are.
Focus on Inputs
<input
class="border border-gray-300 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-300 px-3 py-2 rounded"
placeholder="Your email"
>Focus Visual Change Diagram
Before focus (user has not clicked):
┌──────────────────────────────┐
│ Your email │ ← gray border
└──────────────────────────────┘
After focus (user clicks or tabs in):
┌══════════════════════════════╗
║ Your email ║ ← blue border + ring
╚══════════════════════════════╝Focus on Buttons
<button class="bg-green-500 text-white px-4 py-2 rounded focus:outline-none focus:ring-4 focus:ring-green-300">
Submit
</button>Focus-Visible
Standard focus triggers even on mouse clicks. focus-visible triggers only when the user navigates with a keyboard. This prevents the ring from appearing on mouse clicks while still helping keyboard users.
<button class="focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:outline-none">
Smart focus button
</button>The Active State
Active applies while an element is being pressed — the moment between mousedown and mouseup. It gives instant visual feedback that the press registered.
Active Examples
<button class="bg-blue-500 hover:bg-blue-600 active:bg-blue-800 text-white px-4 py-2 rounded">
Press Me
</button>
<!-- Active with scale-down to simulate physical press -->
<button class="bg-indigo-500 active:scale-95 transition-transform text-white px-6 py-3 rounded-lg">
Press Down
</button>Three-State Button Diagram
State 1 — Idle:
[ bg-blue-500 ]
State 2 — Hovered:
[ bg-blue-600 ] ← slightly darker
State 3 — Pressed (active):
[ bg-blue-800 ] ← much darker = "pressed in"The Visited State
Visited applies to links the user has already opened. It helps users recognize which pages they have already read.
<a class="text-blue-600 visited:text-purple-600" href="/article">
Article Title
</a>Combining Multiple States
You can stack multiple state prefixes on the same element — each state applies independently.
<button class="
bg-blue-500
hover:bg-blue-600
active:bg-blue-800
focus:ring-2
focus:ring-blue-300
focus:outline-none
text-white px-4 py-2 rounded
">
Full interactive button
</button>State Priority Diagram
Idle → bg-blue-500 (base)
Hover → bg-blue-600 (overrides base during hover)
Active → bg-blue-800 (overrides hover during press)
Focus → ring-2 ring-blue-300 (shows at same time as others)Group Hover
Group hover lets a child element change style when the parent is hovered. Like a card where hovering anywhere on the card changes the title color.
<div class="group border rounded-lg p-4 hover:bg-gray-50 cursor-pointer">
<h3 class="text-gray-800 group-hover:text-blue-600 font-semibold">
Card Title
</h3>
<p class="text-gray-500 group-hover:text-gray-700">
Card description text.
</p>
<span class="text-gray-400 group-hover:text-blue-500">
Read more →
</span>
</div>Group Hover Diagram
Mouse NOT on card:
┌──────────────────────────────┐
│ Card Title (gray-800) │
│ Description (gray-500) │
│ Read more → (gray-400) │
└──────────────────────────────┘
Mouse ON card (anywhere):
┌──────────────────────────────┐
│ Card Title (blue-600) ← │ ← group-hover:text-blue-600
│ Description (gray-700) ← │ ← group-hover:text-gray-700
│ Read more → (blue-500) ← │ ← group-hover:text-blue-500
└──────────────────────────────┘Named Groups
When you have nested groups, name them to avoid conflicts.
<div class="group/card ...">
<div class="group/btn ...">
<span class="group-hover/card:text-blue-500">Reacts to card hover</span>
<span class="group-hover/btn:text-red-500">Reacts to btn hover</span>
</div>
</div>Peer State Modifiers
Peer lets a sibling element react to another sibling's state. For example, show an error message when a form input is invalid.
<div>
<input
class="peer border rounded px-3 py-2 invalid:border-red-500"
type="email"
placeholder="Enter email"
>
<p class="hidden peer-invalid:block text-red-500 text-sm mt-1">
Please enter a valid email.
</p>
</div>Peer State Flow
User types: "hello" (invalid email)
Input gets class: invalid → peer-invalid triggers
Error message: hidden → block (appears)
User types: "hello@example.com" (valid)
Input no longer invalid
Error message: block → hidden (disappears)Disabled State
Style elements differently when they are disabled — usually lower opacity or a gray color to signal they cannot be used.
<button
class="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50 disabled:cursor-not-allowed"
disabled
>
Cannot Click
</button>
<input
class="border rounded px-3 py-2 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed"
disabled
>Checked and Selected States
Use these on checkboxes, radio buttons, and select elements.
<input
type="checkbox"
class="accent-blue-500 checked:ring-2 checked:ring-blue-300"
>
<!-- Custom styled checkbox using peer -->
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" class="peer hidden">
<div class="w-5 h-5 border-2 border-gray-300 rounded peer-checked:bg-blue-500 peer-checked:border-blue-500"></div>
<span>Accept terms</span>
</label>Practical Form with All States
<form class="space-y-4 max-w-sm">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input
type="email"
class="w-full border border-gray-300 rounded px-3 py-2
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-blue-400
invalid:border-red-400 invalid:ring-red-200
disabled:bg-gray-100 disabled:cursor-not-allowed"
placeholder="you@example.com"
>
</div>
<button
type="submit"
class="w-full bg-blue-600 text-white py-2 rounded
hover:bg-blue-700
active:bg-blue-900 active:scale-95
focus:outline-none focus:ring-2 focus:ring-blue-300
disabled:opacity-50 disabled:cursor-not-allowed
transition-all duration-150"
>
Subscribe
</button>
</form>Quick Reference: State Prefixes
STATE PREFIX TRIGGERS WHEN
─────────────────────────────────────────────────
Hover hover: cursor is over element
Focus focus: element receives keyboard focus
Focus visible focus-visible: keyboard focus only (not mouse)
Active active: element is being pressed
Visited visited: link has been visited before
Disabled disabled: element has disabled attribute
Checked checked: checkbox/radio is checked
Invalid invalid: input fails HTML validation
Required required: input has required attribute
Placeholder placeholder: styles the placeholder text
Group hover group-hover: parent with "group" is hovered
Peer state peer-*: sibling with "peer" changes stateAccessibility Note
Never remove focus styles completely with focus:outline-none without replacing them. Replace the default outline with a custom ring instead, so keyboard users can always see where focus is.
<!-- Bad: removes focus entirely -->
<button class="focus:outline-none">Button</button>
<!-- Good: replaces with custom ring -->
<button class="focus:outline-none focus:ring-2 focus:ring-blue-500">Button</button>