Chapter 11 of 14 · Tailwind CSS for Beginners — Complete Free Tutorial
Hover, Focus and Other States
Static pages are easy; interfaces earn trust through feedback. This chapter of the Tailwind CSS for Beginners series shows how variants put a condition in front of any utility, which state variants matter most, how to style parents and siblings of the element you are hovering, and what changed in v4's handling of hover and stacking order. The CSS underneath is ordinary pseudo-class styling — :hover, :focus, :disabled — which Tailwind simply wraps in a prefix.
The variant grammar
A variant is a prefix, a colon, and then any utility: hover:bg-indigo-700, focus:ring-2, disabled:opacity-50, md:text-lg. Variants stack from left to right in v4, each one adding a condition, so focus-visible:md:underline reads exactly like the CSS it generates: at medium widths and above, when the element shows a visible focus ring, underline it. v3 stacked these in the opposite direction, so if an old example puts hover on the right of a breakpoint, reverse it.
The state variants you will actually use
| Variant | Matches | Typical pairing |
|---|---|---|
hover: | Pointer over the element | hover:bg-indigo-700 |
focus: / focus-visible: | Element focused, or focused by keyboard | focus-visible:ring-2 |
active: | Being pressed | active:scale-95 |
disabled: | disabled attribute present | disabled:cursor-not-allowed |
checked: | Checkbox or radio on | checked:bg-indigo-600 |
placeholder-shown: | Input still empty | placeholder-shown:border-slate-300 |
required:invalid: | Required field with bad input | required:invalid:border-red-500 |
Structural variants work too: first:rounded-l-lg, last:rounded-r-lg, odd:bg-slate-50, even:bg-white and not-hover:bg-slate-100 all compose with the same colon syntax. There are also attribute and ARIA variants — aria-checked:, data-open: — so a state that lives in markup can drive styling without a single line of JavaScript.
Focus states that stay accessible
Removing focus styles is the most common accessibility failure in styling, and Tailwind makes the right thing easy: give the element a visible ring on keyboard focus, and let mouse clicks stay quiet by using focus-visible:
<input class="rounded-lg border border-slate-300 px-3 py-2 outline-none
focus:border-indigo-500 focus-visible:ring-2 focus-visible:ring-indigo-500/40"
placeholder="Search tutorials">
<button class="rounded-lg bg-slate-900 px-4 py-2 font-semibold text-white
transition hover:bg-slate-700 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-slate-900">
Save
</button>
Expected output:
The input gains a soft indigo halo when tabbed to and a matching
border colour while typing. The button darkens on hover and shows a
crisp ring with a small gap around it during keyboard navigation —
nothing appears after a plain mouse click.
In v4 the plain ring utility is 1px wide (it was 3px in v3, where it was also blue by default), so state the width and colour explicitly: ring-2 ring-indigo-500.
Styling beyond the element: group and peer
Real interactions often involve a parent: hovering a card should recolour its title, which lives in a different element. The group class marks the parent, and group-hover: styles its descendants. peer works sideways for siblings:
<label class="group flex items-center gap-3 rounded-lg border border-slate-200 p-4 hover:border-indigo-400 hover:bg-indigo-50/50">
<input type="checkbox" class="size-4 accent-indigo-600 peer">
<span class="text-slate-700 group-hover:text-indigo-900 peer-checked:font-semibold">Email me about new chapters</span>
</label>
Expected output:
A bordered option row that tints indigo as the mouse moves over it,
with its label darkening at the same moment. Tick the checkbox and
the label turns heavier — a change driven by a sibling, not the
parent.
Both markers are plain group and peer classes with no styles of their own; they exist purely so descendants can reference them. A named group, written group/preview with group-hover/preview:, keeps nested groups from stepping on each other. The same idea extends to children with *: — writing *:opacity-50 on a parent dims everything inside it without a class on a single child.
Transitions: making states feel deliberate
A state change that snaps looks broken; a state change that eases feels intentional. Tailwind bundles the whole transition vocabulary as short utilities:
<button class="rounded-lg bg-indigo-600 px-4 py-2 font-semibold text-white
transition duration-200 ease-out hover:bg-indigo-700
active:scale-95 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
Commit changes
</button>
Expected output:
The button darkens smoothly over a fifth of a second, squashes very
slightly while pressed, and shows a clean outline with a gap when
reached by keyboard — each change arriving rather than snapping.
transition alone covers colour, background, border, shadow and outline properties; transition-all and transition-[transform,height] exist for everything else, though animating layout properties still costs more than animating colour or transform. Pair them with duration-150, duration-300 or a custom --animate-* token, and respect motion-reduce:transition-none for users who ask their system for less motion.
What v4 changed about states
- Hover respects devices —
hover:now only applies when the primary input device supports hovering, so touch screens do not get stuck states. - Stacking runs left to right — order-sensitive combinations must be rewritten when you upgrade from v3.
- Important goes last — the exclamation mark moved to the end:
bg-red-500!. - Transitions include outline colour —
transitioncoversoutline-color, so set focus colours unconditionally to avoid a colour flash.
Common mistakes
- Hover-only designs — touch devices have no hover; never hide an action behind it alone.
- Deleting focus styles —
outline-nonewith nofocus-visible:replacement locks keyboard users out. - Forgetting
transition— state changes then snap instantly; addtransitionortransition-colors. - Wrong stacking order —
hover:md:underlinestill works, but writingmd:hover:matches the documentation and your expectations. - Assuming
groupstyles something — it is a marker; the descendant must carrygroup-hover:as well.
Practice checklist
- Build a button with hover, active and
focus-visiblestates and tab to it repeatedly. - Add
disabled:opacity-50 disabled:cursor-not-allowedand toggle the attribute in dev tools. - Create a card whose border, background and title colour all change from one parent hover.
- Tick a checkbox and confirm a sibling label responds with
peer-checked:. - Check the same states on a touch-sized viewport and confirm nothing depends on hover.
States give interfaces life; surfaces give them finish — Borders, Shadows and Backgrounds is next.
Frequently Asked Questions
Tailwind is a utility-first CSS framework: instead of writing custom classes like .card-title, you compose elements from small single-purpose utilities such as text-lg font-semibold text-slate-800. The styles ship as a generated stylesheet containing only the classes your project uses, so the CSS stays small and consistent.
Bootstrap ships a opinionated component layer (buttons, navbars with Bootstrap's own look) that you override with custom CSS. Tailwind ships primitive utilities and lets you design freely — two sites built with Tailwind can look completely different, while two Bootstrap sites tend to look related. You can still build components; you just compose them from utilities first.
You need the fundamentals: the box model, specificity, Flexbox, Grid, media queries and pseudo-classes. Tailwind does not replace CSS knowledge — it renames it. Understanding what padding, margin, gap and z-index mean makes every utility class obvious instead of something to memorise.
v4 is CSS-first: configuration lives in CSS with @import "tailwindcss" and @theme tokens instead of a JavaScript tailwind.config.js, the engine is rewritten in Rust for much faster builds, and modern CSS features (container queries, cascade layers) are built in. This tutorial teaches v4 and notes where v3 differed.
Long class lists are a real trade-off, and the tutorial covers two fixes: consistent ordering (layout → spacing → typography → colour) and small component classes for repeated combinations. The generated CSS contains only classes you actually use — Tailwind removes everything else at build time, so the stylesheet is typically smaller than an unused framework.
Three utilities cover most cases: mx-auto centres a fixed-width block horizontally, flex items-center justify-centre centres children inside a flex parent, and grid place-items-center does the same for grids. Chapter 8 and chapter 9 show both patterns with examples.
Yes — Tailwind is a build step that scans your template files for class names and generates CSS, so it works with any framework or none. It does not ship runtime JavaScript, and the classes look the same in JSX, Blade, PHP or static HTML files.
In v4 you define design tokens in CSS with @theme, for example --color-brand-500: #4f46e5; creates bg-brand-500, text-brand-500 and friends. For a one-off value you can always use arbitrary values like bg-[#0f172a], which need no configuration at all.
A variant prefixes a utility with the condition it applies under: hover:bg-indigo-600 on hover, focus:ring-2 while focused, dark:bg-slate-900 in dark mode, and sm: / md: / lg: at breakpoints. Variants stack, such as md:hover:text-lg, and chapter 11 covers the full pattern.
Yes. Tailwind compiles to a static stylesheet with no JavaScript requirement, and because it ships only used classes, pages stay fast — a Core Web Vitals win that helps SEO. Accessibility, semantic markup and good content remain your job; Tailwind only styles what you write.