Hover, Focus, Active, Disabled, and State Variants
Create clear interactive feedback and style elements based on states, relationships, and user preferences.
Hover, active, and keyboard focus
State variants let you style interaction without writing separate selectors. Examples include hover:bg-blue-700, active:scale-[0.98], and focus-visible:outline-*. Hover is a pointer affordance; focus-visible is essential for keyboard users. Do not remove the browser outline unless you replace it with a visible, sufficiently contrasting focus indicator.
Use transitions sparingly. A small color or shadow transition can clarify feedback, but animation should not delay interaction or make controls feel sluggish.
<button class="rounded-lg bg-blue-600 px-4 py-2.5 font-semibold text-white transition-colors hover:bg-blue-700 active:bg-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
Save changes
</button>focus-visible draws a visible outline for keyboard navigation. transition-colors limits the transition to color-related properties rather than animating everything.Why should a custom button retain a visible focus indicator?
Disabled controls, ARIA states, and forms
State variants can reflect native HTML states, for example disabled:opacity-50, disabled:cursor-not-allowed, invalid:border-red-500, and checked:bg-blue-600. Prefer native controls and native states where possible. An aria-* attribute communicates meaning to assistive technology; Tailwind can style matching attributes, but the attribute itself must be set correctly by the application.
A disabled button should actually have the disabled attribute when the action cannot be used. Styling a button to look disabled without disabling it creates a misleading interface.
A red border alone may not explain the problem. Add clear error text, connect it with aria-describedby, and validate on the server as well as the client.
<label class="block">
<span class="text-sm font-medium text-slate-800">Work email</span>
<input type="email" required aria-describedby="email-help"
class="mt-2 w-full rounded-lg border border-slate-300 px-3 py-2.5 outline-none focus-visible:ring-2 focus-visible:ring-blue-600 invalid:border-red-500"
placeholder="you@example.com">
<span id="email-help" class="mt-1 block text-sm text-slate-600">Use your company email address.</span>
</label>
<button disabled class="mt-4 rounded-lg bg-blue-600 px-4 py-2.5 font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50">
Submit request
</button>required constraint. The invalid border reflects browser validation state, and the focus ring makes keyboard focus visible. In a production form, explain validation errors in text and associate them with the relevant input.Group, peer, and attribute variants
group and peer let a child respond to the state of a related element without custom JavaScript. Add group to a parent and use a variant such as group-hover: on a descendant. peer styles a following sibling based on a preceding peer's state, so DOM order matters.
Attribute variants such as aria-expanded: can reflect application state. They are especially useful for menus and disclosure controls, but styling does not implement the behavior: JavaScript must still toggle the attribute, manage focus, and support keyboard interaction.
<a href="#project" class="group block rounded-xl border p-4 transition hover:border-blue-400 hover:bg-blue-50">
<div class="flex items-center justify-between">
<h3 class="font-semibold text-slate-900 group-hover:text-blue-700">Project dashboard</h3>
<span class="transition-transform group-hover:translate-x-1" aria-hidden="true">→</span>
</div>
<p class="mt-2 text-sm text-slate-600">Open tasks, milestones, and team activity.</p>
</a>
<label class="mt-4 flex items-center gap-2">
<input class="peer size-4" type="checkbox">
<span class="text-sm text-slate-700 peer-checked:font-semibold">Include archived tasks</span>
</label>group-hover:. The checkbox is the peer, and its following label text changes weight when checked. These variants style relationships; they do not replace semantic markup.- Use focus-visible styles for keyboard accessibility.
- Prefer native HTML states over simulated states.
- Group and peer variants depend on the DOM relationship and order.