zudo-css

Type to search...

to open search from anywhere

Hover, Focus, and Active States

CreatedMar 13, 2026UpdatedMar 26, 2026Takeshi Takatsudo

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

Revision History