3 Topics
Chapter•55 Minutes

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.

Accessible button states
<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>
💡 The default state has strong contrast. Hover and active states provide pointer feedback, while focus-visible draws a visible outline for keyboard navigation. transition-colors limits the transition to color-related properties rather than animating everything.
Knowledge Check

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.

Visual validation is not enough

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.

Form field with validation styling
<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>
💡 The input has a native email type and 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.

Group hover and peer state
<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>
💡 The descendant title and arrow react to the card's hover state through 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.
Key Takeaways
  • 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.