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.
Discussion
Loading…