Tailwind CSS

Hover, Focus & State Variants

20 min Lesson 11 of 35

Hover, Focus & State Variants in Tailwind CSS

Tailwind CSS provides powerful state variant modifiers that allow you to conditionally apply styles based on user interaction states, element states, and more. These variants make it incredibly easy to create interactive and accessible user interfaces without writing custom CSS.

Basic Interaction States

The most common state variants are for mouse interactions. Tailwind makes it simple to change styles when users hover over, click on, or focus on elements.

Hover State

The hover: modifier applies styles when the user hovers over an element with their mouse:

Basic Hover Examples

<!-- Hover background change -->
<button class="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2 rounded">
    Hover Me
</button>

<!-- Hover text color change -->
<a href="#" class="text-blue-600 hover:text-blue-800 underline">
    Hover Link
</a>

<!-- Multiple hover effects -->
<div class="bg-white hover:bg-gray-100 hover:shadow-lg transition-all p-4">
    Hover for multiple effects
</div>

<!-- Hover scale effect -->
<img src="image.jpg" class="hover:scale-110 transition-transform" alt="Image">
Note: The hover: modifier works on any utility class. You can combine it with colors, spacing, transforms, shadows, and more.

Focus State

The focus: modifier applies styles when an element receives keyboard focus, essential for accessibility:

Focus State Examples

<!-- Input field with focus styles -->
<input
    type="text"
    class="border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 px-4 py-2 rounded"
    placeholder="Focus on me"
>

<!-- Button with focus ring -->
<button class="bg-green-500 text-white px-4 py-2 rounded focus:outline-none focus:ring-4 focus:ring-green-300">
    Click Me
</button>

<!-- Link with focus styles -->
<a href="#" class="text-blue-600 focus:text-blue-800 focus:underline">
    Tab to focus
</a>
Accessibility Tip: Always provide visible focus indicators for keyboard navigation. The focus:ring utilities are perfect for this.

Active State

The active: modifier applies styles when an element is being clicked or pressed:

Active State Examples

<!-- Button with active state -->
<button class="bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white px-6 py-3 rounded">
    Press Me
</button>

<!-- Scale down on click -->
<button class="bg-green-500 active:scale-95 transition-transform px-4 py-2 rounded text-white">
    Click to scale
</button>

<!-- Combined states -->
<button class="bg-purple-500 hover:bg-purple-600 active:bg-purple-800 focus:ring-4 focus:ring-purple-300 text-white px-4 py-2 rounded">
    All States
</button>

Advanced Focus Variants

Focus-Within

The focus-within: modifier applies styles to a parent element when any of its children receive focus:

Focus-Within Examples

<!-- Form container that highlights when any input is focused -->
<div class="border-2 border-gray-300 focus-within:border-blue-500 focus-within:shadow-lg p-4 rounded">
    <label class="block mb-2">Name</label>
    <input type="text" class="border px-3 py-2 rounded w-full">
</div>

<!-- Search container with focus-within -->
<div class="flex items-center bg-gray-100 focus-within:bg-white focus-within:ring-2 focus-within:ring-blue-300 rounded-lg px-4 py-2">
    <svg class="w-5 h-5 text-gray-400">...</svg>
    <input type="search" class="bg-transparent focus:outline-none ml-2" placeholder="Search...">
</div>

Focus-Visible

The focus-visible: modifier only applies styles when an element receives keyboard focus, not mouse click focus:

Focus-Visible Examples

<!-- Only shows focus ring when tabbing, not clicking -->
<button class="bg-blue-500 text-white px-4 py-2 rounded focus:outline-none focus-visible:ring-4 focus-visible:ring-blue-300">
    Tab to see ring
</button>

<!-- Link with keyboard-only focus indicator -->
<a href="#" class="text-blue-600 focus-visible:underline focus-visible:ring-2 focus-visible:ring-blue-300 rounded">
    Keyboard focus only
</a>
Note: focus-visible: is better for user experience because it doesn't show focus rings when clicking with a mouse, but still provides them for keyboard users.

Group and Peer Modifiers

Group Hover

The group and group-hover: utilities allow you to style child elements based on the parent's hover state:

Group Hover Examples

<!-- Card with group hover effects -->
<div class="group bg-white hover:bg-blue-50 p-6 rounded-lg shadow transition-all">
    <h3 class="text-gray-800 group-hover:text-blue-600 text-xl font-bold">
        Card Title
    </h3>
    <p class="text-gray-600 group-hover:text-gray-800 mt-2">
        Hover over the card to see changes
    </p>
    <button class="bg-blue-500 group-hover:bg-blue-600 text-white px-4 py-2 rounded mt-4">
        Learn More
    </button>
</div>

<!-- Navigation with group hover -->
<a href="#" class="group flex items-center gap-3 p-3 rounded hover:bg-gray-100">
    <svg class="w-6 h-6 text-gray-400 group-hover:text-blue-600">...</svg>
    <span class="text-gray-700 group-hover:text-gray-900 group-hover:font-semibold">
        Dashboard
    </span>
    <svg class="w-4 h-4 text-gray-400 group-hover:translate-x-1 transition-transform ml-auto">→</svg>
</a>

<!-- Image card with overlay -->
<div class="group relative overflow-hidden rounded-lg">
    <img src="image.jpg" class="group-hover:scale-110 transition-transform duration-300" alt="Image">
    <div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-50 transition-all">
        <div class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
            <button class="bg-white text-gray-900 px-6 py-3 rounded-lg font-semibold">
                View Details
            </button>
        </div>
    </div>
</div>
Pro Tip: Group hover is perfect for cards, navigation items, and any component where you want multiple child elements to respond to the parent being hovered.

Peer Modifiers

The peer and peer-*: utilities allow you to style an element based on the state of a sibling element:

Peer Modifier Examples

<!-- Floating label input -->
<div class="relative">
    <input
        type="text"
        id="email"
        class="peer w-full border-2 border-gray-300 focus:border-blue-500 px-4 py-3 rounded outline-none"
        placeholder=" "
    >
    <label
        for="email"
        class="absolute left-4 top-3 text-gray-500 peer-focus:text-blue-500 peer-focus:text-sm peer-focus:-top-6 peer-focus:left-0 transition-all peer-[:not(:placeholder-shown)]:text-sm peer-[:not(:placeholder-shown)]:-top-6 peer-[:not(:placeholder-shown)]:left-0"
    >
        Email Address
    </label>
</div>

<!-- Checkbox with peer styling -->
<div class="flex items-center gap-3">
    <input type="checkbox" id="terms" class="peer w-5 h-5">
    <label for="terms" class="text-gray-600 peer-checked:text-blue-600 peer-checked:font-semibold">
        I agree to the terms and conditions
    </label>
</div>

<!-- Radio button with custom styling -->
<div class="flex items-start gap-3">
    <input type="radio" id="option1" name="option" class="peer sr-only">
    <label for="option1" class="flex items-center gap-3 p-4 border-2 border-gray-300 peer-checked:border-blue-500 peer-checked:bg-blue-50 rounded-lg cursor-pointer">
        <div class="w-6 h-6 rounded-full border-2 border-gray-300 peer-checked:border-blue-500 flex items-center justify-center">
            <div class="w-3 h-3 rounded-full bg-blue-500 hidden peer-checked:block"></div>
        </div>
        <div>
            <div class="font-semibold text-gray-900">Option 1</div>
            <div class="text-sm text-gray-600">Description of option 1</div>
        </div>
    </label>
</div>
Note: Peer modifiers only work with siblings. The peer element must come before the element you want to style in the HTML structure.

Structural Pseudo-Class Variants

First, Last, Odd, and Even

Tailwind provides variants for styling elements based on their position within a parent:

Structural Variants Examples

<!-- List with first and last styling -->
<ul class="divide-y divide-gray-200">
    <li class="py-3 first:pt-0 last:pb-0">First Item</li>
    <li class="py-3 first:pt-0 last:pb-0">Second Item</li>
    <li class="py-3 first:pt-0 last:pb-0">Third Item</li>
    <li class="py-3 first:pt-0 last:pb-0">Last Item</li>
</ul>

<!-- Table with odd/even row colors -->
<table class="w-full">
    <tbody>
        <tr class="odd:bg-white even:bg-gray-50">
            <td class="px-4 py-3">Row 1</td>
        </tr>
        <tr class="odd:bg-white even:bg-gray-50">
            <td class="px-4 py-3">Row 2</td>
        </tr>
        <tr class="odd:bg-white even:bg-gray-50">
            <td class="px-4 py-3">Row 3</td>
        </tr>
    </tbody>
</table>

<!-- Navigation breadcrumbs -->
<nav class="flex items-center gap-2">
    <a href="#" class="text-blue-600 hover:underline">Home</a>
    <span class="text-gray-400">/</span>
    <a href="#" class="text-blue-600 hover:underline">Products</a>
    <span class="text-gray-400">/</span>
    <span class="text-gray-600">Current Page</span>
</nav>

<!-- Grid with different first item -->
<div class="grid grid-cols-3 gap-4">
    <div class="first:col-span-3 first:row-span-2 bg-gray-200 p-4 rounded">
        Featured Item
    </div>
    <div class="bg-gray-100 p-4 rounded">Item 2</div>
    <div class="bg-gray-100 p-4 rounded">Item 3</div>
    <div class="bg-gray-100 p-4 rounded">Item 4</div>
</div>

Form State Variants

Disabled State

The disabled: modifier styles form elements when they are disabled:

Disabled State Examples

<!-- Disabled button -->
<button
    disabled
    class="bg-blue-500 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed text-white px-4 py-2 rounded"
>
    Disabled Button
</button>

<!-- Disabled input -->
<input
    type="text"
    disabled
    value="Disabled input"
    class="border border-gray-300 disabled:bg-gray-100 disabled:text-gray-500 disabled:cursor-not-allowed px-4 py-2 rounded w-full"
>

<!-- Conditionally disabled -->
<button
    class="bg-green-500 hover:bg-green-600 disabled:bg-gray-300 disabled:hover:bg-gray-300 disabled:cursor-not-allowed text-white px-6 py-3 rounded transition-colors"
    disabled
>
    Submit
</button>

Required State

The required: modifier styles form elements marked as required:

Required State Examples

<!-- Required input with indicator -->
<div class="relative">
    <input
        type="text"
        required
        class="border-2 border-gray-300 required:border-blue-300 focus:border-blue-500 px-4 py-2 rounded w-full"
    >
    <span class="absolute right-3 top-3 text-red-500 required:block hidden">*</span>
</div>

<!-- Form with required fields -->
<form class="space-y-4">
    <div>
        <label class="block text-gray-700 mb-2">
            Name <span class="text-red-500">*</span>
        </label>
        <input
            type="text"
            required
            class="border border-gray-300 required:border-l-4 required:border-l-blue-500 focus:border-blue-500 px-4 py-2 rounded w-full"
        >
    </div>
</form>

Placeholder State

The placeholder: modifier styles the placeholder text in input fields:

Placeholder State Examples

<!-- Custom placeholder styling -->
<input
    type="text"
    placeholder="Enter your email"
    class="border border-gray-300 focus:border-blue-500 placeholder:text-gray-400 placeholder:italic px-4 py-2 rounded w-full"
>

<!-- Placeholder with custom color -->
<input
    type="search"
    placeholder="Search..."
    class="bg-gray-100 placeholder:text-gray-500 px-4 py-2 rounded-full w-full"
>

<!-- Textarea with styled placeholder -->
<textarea
    placeholder="Type your message here..."
    class="border border-gray-300 placeholder:text-gray-400 placeholder:text-sm focus:border-blue-500 px-4 py-2 rounded w-full h-32"
></textarea>

File Input State

The file: modifier styles the file input button:

File Input Examples

<!-- Styled file input -->
<input
    type="file"
    class="block w-full text-sm text-gray-500
        file:mr-4 file:py-2 file:px-4
        file:rounded-full file:border-0
        file:text-sm file:font-semibold
        file:bg-blue-50 file:text-blue-700
        hover:file:bg-blue-100
        file:cursor-pointer cursor-pointer"
>

<!-- Multiple file upload -->
<input
    type="file"
    multiple
    class="block w-full text-sm text-gray-600
        file:mr-4 file:py-3 file:px-6
        file:rounded-lg file:border file:border-gray-300
        file:text-sm file:font-medium
        file:bg-white file:text-gray-700
        hover:file:bg-gray-50
        file:transition-colors"
>

Link State Variants

Visited State

The visited: modifier styles links that have been visited:

Visited State Examples

<!-- Links with visited state -->
<a href="#page1" class="text-blue-600 visited:text-purple-600 hover:underline">
    Link 1 (changes color when visited)
</a>

<a href="#page2" class="text-blue-600 visited:text-gray-600 visited:line-through">
    Link 2 (shows as read)
</a>

<!-- Article links -->
<ul class="space-y-2">
    <li>
        <a href="#article1" class="text-blue-600 visited:text-purple-700 visited:opacity-75 hover:underline">
            Article 1 - Introduction to Tailwind
        </a>
    </li>
    <li>
        <a href="#article2" class="text-blue-600 visited:text-purple-700 visited:opacity-75 hover:underline">
            Article 2 - Advanced Techniques
        </a>
    </li>
</ul>

Stacking State Variants

You can combine multiple state variants to create complex interactions:

Combined State Examples

<!-- Button with all states -->
<button class="
    bg-blue-500
    hover:bg-blue-600
    active:bg-blue-700
    focus:outline-none
    focus:ring-4
    focus:ring-blue-300
    disabled:bg-gray-300
    disabled:cursor-not-allowed
    disabled:hover:bg-gray-300
    text-white
    font-semibold
    px-6
    py-3
    rounded-lg
    transition-all
">
    Interactive Button
</button>

<!-- Card with group and individual states -->
<div class="group relative bg-white hover:shadow-2xl transition-shadow rounded-lg overflow-hidden">
    <img
        src="image.jpg"
        class="w-full group-hover:scale-105 transition-transform duration-300"
        alt="Card"
    >
    <div class="p-6">
        <h3 class="text-xl font-bold text-gray-900 group-hover:text-blue-600 transition-colors">
            Card Title
        </h3>
        <p class="text-gray-600 mt-2 group-hover:text-gray-800">
            Card description text
        </p>
        <button class="
            mt-4
            bg-blue-500
            hover:bg-blue-600
            active:scale-95
            focus:ring-4
            focus:ring-blue-300
            text-white
            px-4
            py-2
            rounded
            transition-all
        ">
            Learn More
        </button>
    </div>
</div>

<!-- Form input with multiple states -->
<input
    type="email"
    required
    placeholder="Enter email"
    class="
        w-full
        border-2
        border-gray-300
        focus:border-blue-500
        focus:ring-2
        focus:ring-blue-200
        invalid:border-red-500
        invalid:focus:ring-red-200
        disabled:bg-gray-100
        disabled:cursor-not-allowed
        placeholder:text-gray-400
        placeholder:italic
        px-4
        py-3
        rounded-lg
        outline-none
        transition-all
    "
>
Best Practice: When stacking multiple state variants, order them from most general to most specific. For example: hover:bg-blue-600 focus:bg-blue-700 active:bg-blue-800

Exercise 1: Interactive Navigation Menu

Create a sidebar navigation menu with the following features:

  • Use group hover to highlight the entire item when hovering
  • Change icon color on group hover
  • Add a sliding indicator that appears on hover
  • Style the active/current page differently
  • Include focus states for keyboard navigation

Exercise 2: Advanced Form with State Variants

Build a registration form that includes:

  • Floating labels that move when input is focused or has content
  • Custom styled checkboxes using peer modifiers
  • Different border colors for required, focused, and invalid states
  • Disabled submit button until form is valid
  • Styled file upload button

Exercise 3: Product Card Grid

Create a responsive product card grid where each card:

  • Scales up slightly on hover with smooth transition
  • Shows an overlay with "Quick View" button on hover
  • Has an "Add to Cart" button that changes on hover, focus, and active states
  • Displays a "Sale" badge on the first item only
  • Shows visited links in a different color

Summary

In this lesson, you've learned about Tailwind's powerful state variant system:

  • Basic interaction states: hover, focus, active, visited
  • Advanced focus variants: focus-within, focus-visible
  • Group and peer modifiers: styling based on parent or sibling state
  • Structural variants: first, last, odd, even
  • Form states: disabled, required, placeholder, file input
  • Stacking variants: combining multiple states for complex interactions

State variants are essential for creating interactive and accessible user interfaces. They allow you to add sophisticated behaviors without writing custom CSS, making your development process faster and more maintainable.