Practical Animation Tips
Apply the sections relevant to the requested interaction. These are design defaults; the product's established motion language and the user's requirements guide the result.
Workflow
- Identify the affected interaction and its expected feedback. For a bug, reproduce the reported behavior before choosing a pattern below.
- Read the relevant pattern and make the smallest change that achieves that feedback.
Use
modern-web-guidancebefore changing HTML, CSS, or client-side JavaScript. - Verify the affected states in the available browser, including repeated interaction,
keyboard use, and reduced-motion behavior where applicable. Use
agent-browserfor browser interaction. Record frame by frame only when normal playback leaves a defect unclear.
Completion: the requested feedback works, the reported defect no longer reproduces, and relevant interaction checks pass. State any check the environment prevented.
Recording & Debugging
Record Your Animations
When something feels off but you can't identify why, record the animation and play it back frame by frame. This reveals details invisible at normal speed.
Fix Shaky Animations
Elements may shift by 1px at the start/end of CSS transform animations due to GPU/CPU rendering handoff.
Fix:
.element {
will-change: transform;
}
This tells the browser to keep the element on the GPU throughout the animation.
Button & Click Feedback
Scale Buttons on Press
Make interfaces feel responsive by adding subtle scale feedback:
button:active {
transform: scale(0.97);
}
This gives instant visual feedback that the interface is listening.
Don't Animate from scale(0)
Starting from scale(0) makes elements appear from nowhere—it feels unnatural.
Bad:
.element {
transform: scale(0);
}
.element.visible {
transform: scale(1);
}
Good:
.element {
transform: scale(0.95);
opacity: 0;
}
.element.visible {
transform: scale(1);
opacity: 1;
}
Elements should always have some visible shape, like a deflated balloon.
Tooltips & Popovers
Skip Animation on Subsequent Tooltips
First tooltip: delay + animation. Subsequent tooltips (while one is open): instant, no delay.
.tooltip {
transition:
transform 125ms ease-out,
opacity 125ms ease-out;
transform-origin: var(--transform-origin);
}
.tooltip[data-starting-style],
.tooltip[data-ending-style] {
opacity: 0;
transform: scale(0.97);
}
/* Skip animation for subsequent tooltips */
.tooltip[data-instant] {
transition-duration: 0ms;
}
Radix UI and Base UI support this pattern with data-instant attribute.
Make Animations Origin-Aware
Popovers should scale from their trigger, not from center.
/* Default (wrong for most cases) */
.popover {
transform-origin: center;
}
/* Correct - scale from trigger */
.popover {
transform-origin: var(--transform-origin);
}
Radix UI:
.popover {
transform-origin: var(--radix-dropdown-menu-content-transform-origin);
}
Base UI:
.popover {
transform-origin: var(--transform-origin);
}
Speed & Timing
Keep Animations Fast
A faster-spinning spinner makes apps feel faster even with identical load times. A 180ms select animation feels more responsive than 400ms.
Rule: UI animations should stay under 300ms.
Don't Animate Keyboard Interactions
Arrow key navigation, keyboard shortcuts—these are repeated hundreds of times daily. Animation makes them feel slow and disconnected.
Never animate:
- List navigation with arrow keys
- Keyboard shortcut responses
- Tab/focus movements
Be Careful with Frequently-Used Elements
A hover effect is nice, but if triggered multiple times a day, it may benefit from no animation at all.
Check: Repeat the affected interaction during verification and assess whether its animation delays feedback or interrupts the user's task.
Hover States
Fix Hover Flicker
When hover animation changes element position, the cursor may leave the element, causing flicker.
Problem:
.box:hover {
transform: translateY(-20%);
}
Solution: Animate a child element instead:
<div class="box">
<div class="box-inner"></div>
</div>
.box:hover .box-inner {
transform: translateY(-20%);
}
.box-inner {
transition: transform 200ms ease;
}
The parent's hover area stays stable while the child moves.
Disable Hover on Touch Devices
Touch devices don't have true hover. Accidental finger movement triggers unwanted hover states.
@media (hover: hover) and (pointer: fine) {
.card:hover {
transform: scale(1.05);
}
}
Note: Tailwind v4's hover: class automatically applies only when the device supports hover.
Touch & Accessibility
Ensure Appropriate Target Areas
Small buttons are hard to tap. Use a pseudo-element to create larger hit areas without changing layout.
Minimum target: 44px (Apple and WCAG recommendation)
@utility touch-hitbox {
position: relative;
}
@utility touch-hitbox::before {
content: "";
position: absolute;
display: block;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
min-height: 44px;
min-width: 44px;
z-index: 9999;
}
Usage:
<button className="touch-hitbox">
<BellIcon />
</button>
Easing Selection
Use ease-out for Enter/Exit
Elements entering or exiting should use ease-out. The fast start creates responsiveness.
.dropdown {
transition:
transform 200ms ease-out,
opacity 200ms ease-out;
}
ease-in starts slow—wrong for UI. Same duration feels slower because the movement is back-loaded.
Use ease-in-out for On-Screen Movement
Elements already visible that need to move should use ease-in-out. Mimics natural acceleration/deceleration like a car.
.slider-handle {
transition: transform 250ms ease-in-out;
}
Use Custom Easing Curves
Built-in CSS curves are usually too weak. Custom curves create more intentional motion.
Resources:
Visual Tricks
Use Blur as a Fallback
When easing and timing adjustments don't solve the problem, add subtle blur to mask imperfections.
.button-transition {
transition:
transform 150ms ease-out,
filter 150ms ease-out;
}
.button-transition:active {
transform: scale(0.97);
filter: blur(2px);
}
Blur bridges visual gaps between states, tricking the eye into seeing smoother transitions. The two states blend instead of appearing as distinct objects.
Performance note: Keep blur under 20px, especially on Safari.