Home / Articles / Animation & Motion /

animationtransitions

Common transition patterns: hover, focus, state

A cookbook of transition recipes you'll reach for daily — hover lifts, focus rings, active presses, and toggled component states.

The hover lift

The most universal interactive pattern: a card or button rises on hover with a deepening shadow. Combine transform and box-shadow for the illusion of elevation.

.card {
  background: oklch(0.19 0.02 260);
  box-shadow: 0 1px 3px oklch(0% 0 0 / 0.1);
  transform: translateY(0);
  transition:
    transform 0.2s ease,
    box-shadow 0.25s ease;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px oklch(0% 0 0 / 0.18);
}

Keep the lift small — 2 to 6 pixels. Larger values feel cartoonish in production UIs.

Focus-visible ring

Keyboard users need visible focus indicators. Use :focus-visible to show a ring only on keyboard navigation, not mouse clicks.

.btn {
  outline: 2px solid oklch(0% 0 0 / 0);
  outline-offset: 2px;
  transition:
    outline-color 0.15s ease,
    outline-offset 0.15s ease;
}

.btn:focus-visible {
  outline-color: oklch(0.72 0.19 265);
  outline-offset: 4px;
}

Transitioning outline-offset creates a subtle expansion effect. The ring grows outward from the element, drawing attention without layout shift.

Active press

The :active state fires on mouse-down. A quick scale-down gives tactile feedback, like pressing a physical button.

.btn {
  background: oklch(0.52 0.22 265);
  color: oklch(1 0 0);
  transform: scale(1);
  transition: transform 0.1s ease;
}

.btn:hover {
  background: oklch(0.48 0.24 265);
}

.btn:active {
  transform: scale(0.96);
  transition-duration: 0.05s;
}

Notice the shorter duration on :active. The press should feel instant — 50 ms is enough. The release animates back at the original 100 ms.

Color swap on hover

Swapping foreground and background colors on hover creates a bold interactive style for links and tags.

.tag {
  background: oklch(0.19 0.02 260);
  color: oklch(0.72 0.19 265);
  border: 1px solid oklch(0.72 0.19 265 / 0.3);
  transition:
    background 0.2s ease,
    color 0.2s ease;
}

.tag:hover {
  background: oklch(0.52 0.22 265);
  color: oklch(1 0 0);
}

Toggled component state

Class-based state changes — toggled by JavaScript — use the same transition mechanics. The transition is declared once on the base state.

.accordion-body {
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  transition:
    max-height 0.35s ease,
    opacity 0.25s ease;
}

.accordion.is-open .accordion-body {
  max-height: 500px;
  opacity: 1;
}
Using max-height with a large value is a classic workaround. For smoother results, consider calc-size(auto) where supported.

Asymmetric enter and exit timing

Often the enter transition should feel different from the exit. Declare different transitions on the base state and the active state.

.tooltip {
  opacity: 0;
  transform: translateY(4px);
  /* Exit: quick fade */
  transition:
    opacity 0.1s ease,
    transform 0.1s ease;
}

.trigger:hover .tooltip {
  opacity: 1;
  transform: translateY(0);
  /* Enter: slower, with deceleration */
  transition:
    opacity 0.2s ease-out,
    transform 0.2s ease-out;
}

The tooltip appears slowly and deliberately but vanishes quickly. This matches user expectation — content should arrive with weight and leave without lingering.

Reduced motion fallback

Wrap every interactive transition in a reduced-motion query. Replace spatial movement with opacity-only fades or remove transitions entirely.

@media (prefers-reduced-motion: reduce) {
  .card,
  .btn,
  .tooltip {
    transition-duration: 0.01s;
  }
}

Setting duration to 0.01s instead of 0s preserves transitionend events that JavaScript might depend on, while appearing instant to the user.