Skip to main content
  • Created:
  • Updated:
  • Author:
    Takeshi Takatsudo

Hover, Focus, and Active States

The Problem

Interactive elements need visual feedback for hover, focus, and active states. AI agents commonly add :hover styles that create sticky hover states on touch devices, omit :focus and :focus-visible styling (breaking keyboard accessibility), and apply identical styles to all three states when they should be distinct. The result is an interface that works with a mouse but frustrates touch users and keyboard navigators.

The Solution

Style each interaction state with purpose, use @media (hover: hover) to scope hover effects to devices that support them, and use :focus-visible instead of :focus for keyboard-only focus indicators.

The Three States

  • :hover — A pointing device is over the element (mouse, trackpad). Does not apply reliably on touch devices.
  • :focus — The element is focused, whether by mouse click, keyboard tab, or programmatic focus.
  • :focus-visible — The element is focused and the browser determines a visible indicator is appropriate (typically keyboard navigation). Mouse clicks on buttons do not trigger :focus-visible.
  • :active — The element is being activated (mouse button held down, finger pressing on touch).
Button States — Hover and Tab to see states

Code Examples

Basic Button States

.button {
background-color: var(--color-primary, #2563eb);
color: white;
border: 2px solid transparent;
padding: 0.625rem 1.25rem;
border-radius: 0.375rem;
cursor: pointer;
transition: background-color 0.15s ease, transform 0.1s ease;
}

/* Hover: only on devices that support it */
@media (hover: hover) {
.button:hover {
background-color: var(--color-primary-dark, #1d4ed8);
}
}

/* Focus-visible: keyboard focus indicator */
.button:focus-visible {
outline: 2px solid var(--color-primary, #2563eb);
outline-offset: 2px;
}

/* Active: pressed state */
.button:active {
transform: scale(0.97);
}

Link pseudo-classes should follow the LVHA order to avoid specificity conflicts:

a:link {
color: var(--color-link, #2563eb);
text-decoration: underline;
text-underline-offset: 0.2em;
}

a:visited {
color: var(--color-link-visited, #7c3aed);
}

@media (hover: hover) {
a:hover {
color: var(--color-link-hover, #1d4ed8);
text-decoration-thickness: 2px;
}
}

a:focus-visible {
outline: 2px solid var(--color-link, #2563eb);
outline-offset: 2px;
border-radius: 2px;
}

a:active {
color: var(--color-link-active, #1e40af);
}

Card with Hover Effect (Touch-Safe)

.card {
background: var(--color-surface, #ffffff);
border-radius: 0.5rem;
border: 1px solid var(--color-border, #e5e7eb);
padding: 1.5rem;
transition: box-shadow 0.2s ease, transform 0.2s ease;
}

/* Only apply hover elevation on hover-capable devices */
@media (hover: hover) {
.card:hover {
box-shadow: 0 4px 16px rgb(0 0 0 / 0.1);
transform: translateY(-2px);
}
}

/* Keyboard focus */
.card:focus-visible {
outline: 2px solid var(--color-primary, #2563eb);
outline-offset: 2px;
}

Focus-Visible vs. Focus

/* Remove default focus ring for mouse users */
.interactive:focus {
outline: none;
}

/* Show focus ring only for keyboard users */
.interactive:focus-visible {
outline: 2px solid var(--color-primary, #2563eb);
outline-offset: 2px;
}

A safer approach that does not rely on removing :focus styles entirely:

/* Visible focus ring for keyboard navigation */
.interactive:focus-visible {
outline: 2px solid var(--color-primary, #2563eb);
outline-offset: 2px;
}

/* Subtle focus style for mouse clicks (if desired) */
.interactive:focus:not(:focus-visible) {
outline: none;
}

Touch vs. Mouse Input Detection

/* Base interactive styles */
.nav-link {
padding: 0.5rem 1rem;
color: var(--color-text);
text-decoration: none;
}

/* Hover effects only for precise pointers */
@media (hover: hover) and (pointer: fine) {
.nav-link:hover {
background-color: var(--color-surface-hover, #f3f4f6);
}
}

/* Larger touch targets for coarse pointers */
@media (pointer: coarse) {
.nav-link {
min-height: 44px;
display: flex;
align-items: center;
padding: 0.75rem 1rem;
}
}

Form Input Focus States

.input {
border: 1px solid var(--color-border, #d1d5db);
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

/* All focus (mouse and keyboard) gets a border change */
.input:focus {
border-color: var(--color-primary, #2563eb);
box-shadow: 0 0 0 3px rgb(37 99 235 / 0.15);
outline: none;
}

For form inputs, using :focus (not :focus-visible) is usually correct because users need to see which input they are typing into, regardless of how they focused it.

Common AI Mistakes

  • Adding :hover without @media (hover: hover): Hover effects persist as "sticky" on touch devices after a tap, confusing users.
  • Removing :focus outlines without replacement: Writing outline: none on :focus without providing any visible focus indicator, making the page inaccessible to keyboard users.
  • Using :focus instead of :focus-visible: Showing a focus ring on every mouse click (buttons, cards) when only keyboard focus needs a visible indicator.
  • Applying identical styles to all states: Making :hover, :focus, and :active look the same, which removes meaningful visual feedback about the interaction type.
  • Forgetting the LVHA order: Writing :hover before :visited, causing specificity conflicts in link styling.
  • Not testing on touch devices: Assuming hover works everywhere and never verifying the interaction on mobile.
  • Using cursor: pointer on everything: Adding cursor: pointer to non-interactive elements like divs, which misleads users.

When to Use

  • :hover with @media (hover: hover): Visual enhancements (color shifts, shadows, elevation) that only make sense with a mouse or trackpad.
  • :focus-visible: Keyboard focus indicators on buttons, links, and custom interactive elements.
  • :focus: Form inputs where all focus types need a visible indicator.
  • :active: Pressed/tapped feedback (scale, color change) for buttons and interactive elements.
  • @media (pointer: coarse): Increasing touch target sizes and padding for touch devices.

Tailwind CSS

Tailwind provides hover:, focus:, focus-visible:, and active: variant prefixes that map directly to CSS pseudo-classes.

Tailwind: Interactive State Variants

References