Overview
CSS owns motion; reach for JavaScript animation libraries only for cases CSS cannot express. The rules are mechanical: animate transform and opacity, respect motion preferences, and pick transition or @keyframes by whether the change has a clear start state. The umbrella page is css.
Use transition for state changes, @keyframes for sequences
transition interpolates between two values when a property changes. @keyframes steps through values on its own. Hover, focus, open, disabled, and theme swaps are transitions; spinners, loops, and multi-step reveals are keyframes. If you use @keyframes for a hover, a transition is shorter.
.button { transition: background-color 150ms ease, transform 100ms ease; }
.button:hover { transform: translateY(-1px); }
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { animation: spin 1s linear infinite; }Animate transform and opacity
Browsers can animate these on the compositor without re-running layout or paint. Other properties force layout or paint each frame and hurt responsiveness (page-speed).
- Move with
translate(), nottoporleft. - Resize with
scale(), notwidthorheight. - Fade with
opacity, notvisibility.
If you must animate a layout property such as grid-template-rows: 0fr to 1fr, test on a mid-tier phone first.
Respect prefers-reduced-motion
Honor the OS setting. For motion that carries information, such as a progress cue, substitute a static or shorter version instead of removing the cue.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}WCAG 2.2 SC 2.3.3 (Animation from Interactions) is Level AAA, so it is not an AA failure; meet it anyway. Level A SC 2.2.2 applies separately: moving content that starts automatically and lasts over five seconds needs a way to pause, stop, or hide it. See accessibility.
Use will-change sparingly
will-change promotes an element to its own layer ahead of time. Set it just before an animation and clear it afterward; a permanent hint wastes memory and can blur text. Skip it when the animation already runs smoothly.
.menu[data-state="opening"] { will-change: transform, opacity; }
.menu[data-state="open"] { will-change: auto; }Drive timing with tokens and registered properties
Keep durations and easings as custom properties (css-custom-properties). To animate a custom property itself, register it with @property, which is Baseline newly available (July 2024).
:root { --motion-base: 250ms; --ease-out: cubic-bezier(0.2, 0.8, 0.2, 1); }
.dialog { transition: opacity var(--motion-base) var(--ease-out); }Animate entry and display changes with @starting-style
A display: none element does not transition when shown. Use @starting-style for the initial values and transition-behavior: allow-discrete to animate display. Both are Baseline newly available (August 2024).
.toast { transition: opacity 200ms, display 200ms allow-discrete; opacity: 1; }
.toast[hidden] { opacity: 0; display: none; }
@starting-style { .toast { opacity: 0; } }Gate scroll-driven animations
animation-timeline: view() or scroll() ties an animation to scroll position without JavaScript scroll listeners. As of October 2026 it ships in Chrome and Edge (115) and Safari 26, but Firefox has not shipped it unflagged, so the feature has limited availability. Wrap it in @supports and leave the element visible without it.
@supports (animation-timeline: view()) {
.section {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% cover 30%;
}
}
@keyframes reveal { from { opacity: 0; transform: translateY(20px); } }Avoid common pitfalls
- Animating
height: auto. It cannot be interpolated. Use the0frto1frgrid technique, animate to a known value, or useinterpolate-size: allow-keywordswhere available (Chrome and Edge only so far). - Layout thrash. Animating
top,left, orwidthjanks on mobile; convert totransform. - Stuck
will-change. Remove it after the animation ends. - No reduced-motion handling. Ship the media query with the first animation.