Responsive and interactive states
Style interaction states
Use hover, focus-visible, active, disabled, and other state variants without hiding essential feedback from keyboard or touch users.
State variants are prefixes that add a condition to a utility:
<button class="bg-blue-600 hover:bg-blue-700 focus-visible:outline-2 active:translate-y-px disabled:opacity-50">Save</button>
The base bg-blue-600 is always there. hover: kicks in when a pointer is over the button. focus-visible: shows an outline for keyboard focus. active: moves the button down one pixel while it’s pressed. disabled: fades it when the disabled attribute is present.
In the generated CSS these become :hover, :focus-visible, :active, and :disabled pseudo-class selectors. Nothing more.
The state has to be real
A disabled: variant only matches a disabled element:
<button disabled class="disabled:cursor-not-allowed disabled:opacity-50">
Saving…
</button>
Styling a button to look disabled without the attribute is a lie. It still fires clicks, and a screen reader announces it as enabled. The same goes for <div>s with hover styles. A div doesn’t become a button because it changes color. Use the native element and let the variants follow its real state.
Hover is a bonus
Touch users never hover. Some devices emulate hover in strange ways, like sticking after a tap. So treat hover as an enhancement. Anything essential, like an instruction or an action, must also be visible by default or reachable through focus and activation.
Focus is not optional
focus-visible gives keyboard users a clear indicator without flashing an outline after every mouse click. Never strip focus styles because the hover version looks cleaner. Test the outline on every background and at high zoom.
States combine
A disabled button shouldn’t lift on hover. Check that disabled: and hover: don’t fight. For a toggle button, set aria-pressed and style with the aria-pressed: variant:
<button aria-pressed="true" class="aria-pressed:bg-blue-600 aria-pressed:text-white">
Bold
</button>
The visual state follows the accessibility attribute, so they can never disagree.
Keep transitions short
A transition of 150ms feels responsive. Anything long delays feedback. And the state must be clear even when animations don’t run, which we’ll cover in the reduced motion lesson.
Try this: build a button with default, hover, focus-visible, active, disabled, and pressed states. Test it with keyboard, mouse, and touch emulation in DevTools, and confirm the DOM attributes and the visible styles always agree.
Lesson completed