iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Hover / Focus / Active

State variants like hover:, focus:, active:, disabled:, focus-visible:, and aria-* let you style state without writing CSS pseudo-selectors. Layered correctly, they give you accessible, keyboard-friendly interactions in tiny class lists.

hover, focus, focus-visible, ARIA, group

EXAMPLE
<!-- 1) Basic hover/active -->
<button class="
    bg-indigo-600 text-white px-4 py-2 rounded-md
    hover:bg-indigo-700
    active:bg-indigo-800
    transition
">Save</button>

<!-- 2) focus vs focus-visible — accessibility matters -->
<!-- focus       — keyboard OR mouse triggers; rings appear on every click (ugly) -->
<!-- focus-visible — keyboard navigation only; rings appear when keyboard-focused -->
<button class="
    px-4 py-2 rounded-md bg-white text-slate-900 border
    focus:outline-none
    focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500
">Save</button>

<!-- 3) Disabled -->
<button disabled class="
    bg-indigo-600 text-white px-4 py-2 rounded-md
    hover:bg-indigo-700
    disabled:bg-slate-300 disabled:text-slate-500
    disabled:cursor-not-allowed disabled:hover:bg-slate-300
">Save</button>
<!-- disabled:hover:bg-slate-300 explicitly resets hover for the disabled state. -->

<!-- 4) ARIA state-driven styling — pair with proper roles -->
<button aria-pressed="false" class="
    px-3 py-1 rounded border
    aria-pressed:bg-indigo-600 aria-pressed:text-white
    hover:bg-indigo-50 aria-pressed:hover:bg-indigo-700
">Bold</button>

<details class="group">
    <summary class="cursor-pointer">
        Open
        <span class="group-open:rotate-180 inline-block transition-transform">▾</span>
    </summary>
    <div>Hidden until open</div>
</details>

<!-- 5) group — parent-driven child state -->
<a href="#" class="group block p-4 border rounded hover:bg-slate-50">
    <h3 class="text-slate-900 group-hover:text-indigo-600">Title</h3>
    <p class="text-slate-600 group-hover:text-slate-900">Subtitle</p>
    <span class="opacity-0 group-hover:opacity-100 transition">→</span>
</a>

<!-- group-focus, group-disabled, group-aria-*: same shape. -->

<!-- 6) peer — sibling-driven state (preceding sibling only) -->
<label class="block">
    <input type="checkbox" class="peer" />
    <span class="peer-checked:text-green-600">Subscribe</span>
</label>

<input type="email" class="peer" />
<p class="peer-invalid:text-red-600 peer-placeholder-shown:text-slate-400">
    Enter a valid email
</p>

<!-- 7) Named groups + peers (when you need more than one) -->
<div class="group/card">
    <h3 class="group-hover/card:text-indigo-600">Title</h3>
    <div class="group/inner">
        <p class="group-hover/inner:underline">Inner</p>
    </div>
</div>

<!-- 8) Form state pseudos -->
<!--   placeholder-shown        — input not filled (use to render floating-label) -->
<!--   placeholder              — style the placeholder text itself -->
<!--   read-only / required     — semantic form state -->
<!--   valid / invalid          — based on the input's value vs constraints -->
<input class="
    block w-full px-3 py-2 border rounded-md
    placeholder:text-slate-400
    valid:border-green-500
    invalid:border-red-500
    required:border-red-300
    read-only:bg-slate-100
" required pattern="^[^@]+@[^@]+\.[^@]+$" placeholder="Email" />

<!-- 9) Composing with dark mode -->
<button class="
    bg-white text-slate-900 hover:bg-slate-50
    focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500
    dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700
    dark:focus-visible:outline-indigo-400
">Toggle</button>

<!-- 10) Responsive + state stacking -->
<button class="
    bg-indigo-600 text-white px-4 py-2 rounded
    hover:bg-indigo-700
    md:px-6 md:py-3
    md:hover:bg-indigo-800     /* hover only takes effect at md+ */
">Save</button>

<!-- 11) Custom variants in tailwind.config.js -->
<script>
import plugin from 'tailwindcss/plugin';
module.exports = {
    plugins: [
        plugin(({ addVariant }) => {
            addVariant('hocus', ['&:hover', '&:focus']);            // hocus:bg-...
            addVariant('any-hover', '@media (any-hover: hover)');    // styles only when device supports hover
        }),
    ],
};
</script>

<!-- 12) The motion-reduce escape valve -->
<div class="transition motion-reduce:transition-none">
    Respects prefers-reduced-motion automatically.
</div>

<!-- 13) Common bugs -->
<!--   • Using focus: instead of focus-visible: — mouse clicks render rings (ugly UX) -->
<!--   • Group/peer order — peer-* affects FOLLOWING siblings only -->
<!--   • aria-pressed without a real button — JS must toggle the attribute -->
<!--   • Mixing hover and active causing 'sticky' look on touch — pair with @media (hover: hover) -->
<!--   • Forgetting reset on disabled state — disabled:hover:* if base hover is too strong -->
<!--   • Custom variant collisions — name them clearly (hocus, prefers-*) -->

Why it matters

focus-visible: beats focus: for accessibility — rings appear on keyboard navigation, not on every mouse click. Use group for parent-driven child states, peer for sibling reactions, and aria-* variants so state-driven styling stays semantic instead of leaning on extra classes.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
<a class="text-emerald-600 hover:text-emerald-800 focus:ring-2">Link</a>
Try it Yourself »

Discussion

Loading…