Animation bugs usually come from asking motion to change layout. If every frame changes width, height, top, left, margin, or grid tracks, the browser has to recalculate layout while the user is watching.
For most interface motion, start with transform and opacity. Then ask whether the motion is necessary, interruptible, and respectful of user preferences.
Animate the visual layer
.menu {
transform: translateY(-0.5rem);
opacity: 0;
transition:
transform 180ms ease,
opacity 180ms ease;
}
.menu[data-open="true"] {
transform: translateY(0);
opacity: 1;
}
This changes how the menu is painted without recalculating the surrounding layout every frame. It does not make the animation free. A huge fading overlay can still be expensive to paint, and a transformed element can create a new stacking context. The point is to avoid making layout do work that only pixels need to do.
Do not animate from auto
Animating height: 0 to height: auto is not directly interpolable. Common workarounds include max-height hacks, grid row tricks, or JavaScript-measured heights. Each has tradeoffs.
.accordion__panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 180ms ease;
}
.accordion[data-open="true"] .accordion__panel {
grid-template-rows: 1fr;
}
.accordion__panel > div {
overflow: hidden;
}
This pattern can work for simple disclosures, but it still changes layout. Test it with long content and reduced motion. For many forms, settings panels, and documentation pages, instant open is better than fragile motion.
Respect reduced motion
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto;
transition-duration: 1ms;
animation-duration: 1ms;
animation-iteration-count: 1;
}
}
This is not about removing all polish. It is about preventing motion from becoming a barrier. Some interfaces need a different reduced-motion design, not just shorter durations: a slide can become an instant state change, and a shimmer can become a static skeleton.
Reserve will-change
will-change can help the browser prepare for animation, but it is not a performance seasoning to sprinkle everywhere. Leaving it on many elements can increase memory pressure.
.drawer[data-opening="true"] {
will-change: transform;
}
Use it sparingly and close to the interaction if profiling shows a real benefit. For simple CSS-only interactions, it is usually better to skip it than to leave permanent layer hints across the interface.
The production checklist
Before shipping motion, test keyboard interaction, reduced motion, slow devices, and interrupted states. Hover-only animation is not enough. A menu, drawer, or toast must still make sense when opened by keyboard, closed quickly, or skipped by a user who prefers less motion.
When motion feels slow, record a performance trace. If layout appears on every animation frame, inspect the animated properties. If paint is heavy, inspect the size and complexity of the animated layer. Do not optimize from folklore; measure the specific motion in the actual page.
Match motion to ownership
The component that owns state should also own the motion boundary. A drawer can animate its panel, but the page shell should reserve or overlay the space intentionally. A toast can fade and translate, but the notification region should not push the entire interface around unless that is the product behavior. A tab panel can switch instantly while the selected tab uses a small visual transition. Not every state change deserves movement.
Be careful with hover motion. Hover is not available on many touch devices, and cursor-following effects can waste work when no real hover interaction exists. Gate decorative hover transitions with input capability when they are more than simple color changes. The interface should still communicate affordance through visible structure, labels, and focus states.
Motion should also be interruptible. Users click quickly, navigate away, reopen menus, and submit forms while loading states are still active. Avoid CSS that leaves elements inaccessible until an animation finishes. If visibility, pointer events, and focusability change, coordinate those states with the real open or closed state rather than the decorative transition alone.
Use duration as an interface decision, not a brand flourish. Short state transitions can help users understand what changed. Long transitions can make repeated work feel sluggish, especially in dashboards, forms, and navigation. If users trigger the motion many times in one session, keep it quiet and fast. Save expressive motion for moments where it carries meaning.
Prefer one clear movement over several competing effects. Combining scale, blur, shadow, opacity, and position changes can make a simple state change expensive and visually noisy.
Related guides: Custom properties, Stateful UI with :has(), and Fluid spacing without breakpoint sprawl.