Home / Articles / Animation & Motion /
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;
}
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.