AI CSS Animations

Guides · · Maxwell Prehoda

14 CSS Animation Best Practices, Each Shown With a Real Example and a Live Demo

The practices that matter most fit in one sentence: animate transform and opacity, keep motion short and purposeful, ease it, let people interrupt it, and give everyone who asks for less motion a calm version. Almost every janky, annoying or inaccessible animation breaks one of those.

Below, each practice is tied to the source that established it, from web.dev and MDN to Paul Lewis's FLIP technique, Tobias Ahlin's box-shadow trick and Apple's guidelines, and most come with a live snippet from our directory that follows it, so you can open the code and see the practice applied. Timing, easing and reduced motion each get one item here and a full guide of their own.

1. Animate transform and opacity, not layout: Best for any animation that has to stay smooth on phones

Platform: web.dev guide by Kayce Basques and Rachel Andrew

Live from our directory: Underline Grow Link. Copy the HTML and CSS →

web.dev's high-performance animations guide gives the rule every other practice builds on: move elements with `transform` (translate, rotate), resize them with `transform: scale` and show or hide them with `opacity`. It names animating `top` and `left` instead of `transform` as a common cause of slow animations, and advises avoiding any property that triggers layout or paint unless it is absolutely necessary.

The reason is the rendering pipeline. Changing width, height, margin or top makes the browser recalculate layout and repaint on every frame. Transform and opacity can usually be handled by the compositor alone, which is far cheaper and keeps going even when the page is busy.

Our Underline Grow Link below draws its underline with `transform: scaleX()` on a pseudo-element instead of animating `width`. Same look, no layout work.

Watch out for: Scaling text or a bordered box with transform distorts it mid-animation. For short transitions that is invisible; for long ones, scale a wrapper or a pseudo-element rather than the text itself.

2. Tobias Ahlin: animate the opacity of a shadow layer: Best for hover shadows and glows that stutter on mobile

Platform: Blog post, tobiasahlin.com

AI assistant

Drafts, summaries and replies in your own tone.

Live from our directory: Glow Behind Card Hover. Copy the HTML and CSS →

Some effects need an expensive property. Tobias Ahlin's article shows the standard way around it with `box-shadow`, which forces a repaint on every frame if you animate it directly. Put the stronger shadow on a pseudo-element behind the box at `opacity: 0`, then fade that layer in on hover while the box scales with `transform`.

The result looks the same as animating the shadow, but the browser only composites an opacity change. Ahlin notes that the simple version may be fine on desktop while phones or complex layouts stutter, which is exactly where this pays off. The same trick works for gradients, glows and blurred backdrops: draw the end state once, then fade it.

Our Glow Behind Card Hover below is a pure-CSS version of the idea: a blurred gradient pseudo-element under the card fades in by opacity and scale, so the card content stays crisp and the glow never repaints as a shadow.

Watch out for: A large blurred layer is costly to draw the first time, even if animating it is cheap. Keep glow layers small, and avoid stacking many on one screen.

3. Paul Lewis's FLIP for layout changes: Best for reordering lists, expanding cards and shared elements

Platform: Technique, aerotwist.com

What if the thing that changes really is layout, like a card expanding into a panel or a list re-sorting? Paul Lewis's FLIP (First, Last, Invert, Play) is the classic answer. Measure where the element starts, apply the final layout, measure where it ends, apply a transform that puts it back where it started, then animate that transform to zero.

The layout change happens once; the animation is pure transform and opacity. Lewis's framing is that there is a window of about 100 ms after someone interacts in which you can do this measuring work without them noticing, as long as the animation itself then holds its frame rate.

Libraries like Motion's layout animations and GSAP's Flip plugin automate it, and View Transitions (next) bring the same idea into the browser.

Watch out for: FLIP scales elements to fake size changes, which stretches text and rounded corners during the move. Counter-scale children or crossfade content if the distortion shows.

4. View Transitions for state and page changes: Best for page navigations and big UI state swaps

Platform: Browser API (Chrome, Edge, Safari; Firefox same-document only)

The View Transitions API lets the browser do the FLIP work for whole views. In a single-page app, wrap the DOM update in `document.startViewTransition()`; the browser snapshots the old and new states and animates between them. Between pages of a multi-page site, both pages opt in with `@view-transition { navigation: auto; }` and no JavaScript at all.

Chrome's docs list same-document support in Chrome and Edge 111, Firefox 144 and Safari 18, and cross-document support in Chrome and Edge 126 and Safari 18.2, with Firefox not yet supporting cross-document. That makes it a progressive enhancement: unsupported browsers just change instantly.

The transition itself is styled with ordinary CSS animations on pseudo-elements, so every other practice on this page still applies to it.

Watch out for: Snapshots are images. Text inside them does not reflow during the transition, and a long transition on a slow device holds the page in an uninteractive state. Keep view transitions short.

5. will-change as a last resort: Best for fixing a measured stutter, not preventing one

Platform: CSS property (MDN reference)

MDN is blunt: use `will-change` as a last resort for existing performance problems, not to anticipate them. Overusing it makes the browser spend memory on layers you do not need and can slow the page down, and applying it to a large section such as `body` can be bad for performance.

The recommended pattern is to set it shortly before a change and remove it after, from script, rather than leaving it in a stylesheet. web.dev's older performance article gives a rule of thumb: if an animation may start within about 200 ms, it is worth hinting.

In practice, most transform and opacity animations are already fast without it. Reach for it when a profile shows a specific element repainting or stuttering at the start of its animation.

Watch out for: MDN's own example listens for `animationEnd`, but DOM event names are lowercase: `animationend`. Copy that example as written and the hint is never removed.

6. Keep motion off a busy main thread: Best for spinners and loaders that must not freeze

Platform: web.dev article by Paul Lewis and Sam Thorogood

Live from our directory: Ring Spinner. Copy the HTML and CSS →

Styles, layout, paint and JavaScript all share the main thread. web.dev's "Animations and performance" explains that CSS animations and the Web Animations API can usually run on the compositor thread, so they keep moving even when JavaScript is busy. It also makes the more important point: if an animation triggers layout or paint, that cost dwarfs any difference between CSS and JavaScript.

This matters most for loading indicators, which by definition play while the page is working hardest. A spinner driven by `requestAnimationFrame` or by animating `left` freezes exactly when people are watching it. A spinner that only rotates with a CSS keyframe keeps turning.

Our Ring spinner below is one div and one keyframe that rotates with `transform`, on linear timing, so it stays smooth while your app loads.

Watch out for: Composited animations still cost GPU memory and battery. Pause decorative loops that are off screen with `animation-play-state: paused`, or stop rendering them when hidden.

7. Scroll-driven animations instead of scroll listeners: Best for reveal-on-scroll and progress effects

Platform: CSS animation-timeline (Chromium 115+, Safari 26)

Fade
Live from our directory: Fade In On Scroll. Copy the HTML and CSS →

Bramus's Chrome article introduces two new timelines: `scroll()`, which maps a scroller's progress to 0 to 100 percent, and `view()`, which tracks an element as it crosses the viewport. Attach either with `animation-timeline` and ordinary keyframes play as you scroll. The article's point is that scroll-event JavaScript can jank because events arrive asynchronously, while these timelines can run off the main thread.

That replaces a whole category of IntersectionObserver and scroll-listener code for reveals, reading progress bars and parallax-lite effects.

Our Fade In On Scroll below uses `animation-timeline: view()` inside `@supports`, and also plays once on load, so browsers without scroll timelines still see the entrance.

Watch out for: Scroll-driven motion is still motion. Large parallax and anything that moves against the scroll direction are known vestibular triggers; give them a reduced-motion branch that sets `animation: none`.

8. Profile it: paint flashing and the FPS meter: Best for proving an animation is smooth before you ship

Platform: Chrome and Firefox DevTools

Live from our directory: Indeterminate Progress Bar. Copy the HTML and CSS →

Best practices are guesses until you measure. The same web.dev guide walks through the checks: in Chrome's Performance panel, layout work during an animation shows up in the Summary; in Firefox, Recalculate Style entries in the waterfall flag the same problem. Paint flashing in either browser highlights every area that repaints, and Chrome's FPS meter in the Rendering tab shows the share of frames delivered.

Run the animation with paint flashing on. If the moving element flashes green on every frame, it is repainting, and the fix is usually to swap the property for a transform or opacity equivalent. Then repeat the test with CPU throttling, which is closer to a mid-range phone than your laptop is.

Our Progress Bar loader below moves its bar with `transform: translateX()` rather than `left` or `width`, so under paint flashing the bar stays quiet.

Watch out for: A smooth animation on a fast laptop proves very little. Always test with throttling or on a real low-end Android phone.

9. Keep it short: the shortest time that is not jarring: Best for setting durations without guesswork

Platform: NN/g research (Page Laubheimer)

Nielsen Norman Group's rule of thumb is to look for the shortest time an animation can take without being jarring, and it warns that animations are far more often too long than too short. Its ranges: about 100 ms for simple feedback, 200 to 300 ms for substantial changes like a modal, and 400 ms as a ceiling for large movement.

Frequency is the multiplier. Something people see constantly should be shorter and subtler than something they see once.

Our full UI animation duration guide has the token scales from Material, Carbon and GitLab and live demos for each range.

Watch out for: Snippets from demo sites and generated code tend to use showcase timings around 600 ms to a second. Retime them before they go into a product.

10. Ease it: ease-out to enter, ease-in to leave: Best for making motion feel natural instead of mechanical

Platform: NN/g research, CSS easing functions

NN/g's guidance is ease-out for entering elements, ease-in for exiting ones, and avoid linear motion, which reads as mechanical. The CSS keywords are a starting point; stronger named curves such as easeOutQuint, `cubic-bezier(0.22, 1, 0.36, 1)`, usually feel better for entrances.

Linear has one honest use: constant loops like a spinner, where any easing would stutter once per turn.

Our easing function examples cover Material and Carbon curves, overshoot, springs in CSS with `linear()`, and `steps()`, each with a live demo.

Watch out for: An ease-in on an entrance makes an element feel late, then slam into place. If something feels sluggish, check the curve before cutting the duration.

11. Let people interrupt it: Best for toggles, menus and anything that can reverse

Platform: Apple Human Interface Guidelines, CSS transitions

Live from our directory: iOS Toggle Switch. Copy the HTML and CSS →

Apple's guidelines say to let people cancel motion: as much as possible, do not make them wait for an animation to complete before they can act, especially one they will see again. On the web that comes down to two choices: never block input while something animates, and use the right CSS tool for state changes.

CSS transitions are built for this. If a hover or toggle state flips back halfway through, the transition reverses from where the element currently is. A keyframe animation started by a class restarts from its first keyframe instead, which is why a menu built on keyframes jumps when you reopen it while it closes. Use transitions for state, keyframes for choreography and loops.

Our iOS Toggle Switch below is built on transitions, so tapping it again mid-slide sends the knob straight back.

Watch out for: Disabling a button until its animation ends is the most common way an animation turns into a wait. Accept the next click and let the motion catch up.

12. Respect prefers-reduced-motion: Best for shipping motion that is safe for everyone

Platform: CSS media query, WCAG 2.2 (SC 2.3.3, 2.2.2)

People with vestibular disorders can get dizzy or nauseous from large motion, parallax in particular, as Thomas Steiner's web.dev article explains. The `prefers-reduced-motion: reduce` media query tells you who asked for less, and WCAG 2.3.3 lists using it as a sufficient technique. Separately, WCAG 2.2.2 (Level A) requires a way to pause anything that moves for more than five seconds alongside other content.

Good reduced versions replace movement with a short fade rather than removing every change, and keep status animations like spinners, slower.

Our prefers-reduced-motion examples show how Apple, GitHub and the Animal Crossing site handle it, plus the CSS, React and GSAP patterns.

Watch out for: CSS-only guards do nothing for JavaScript, canvas or video animation. Read the preference with `matchMedia` too, and listen for changes.

13. Our Fade In Left: split the fade from the move: Best for entrances where text must be readable early

Platform: Motions directory, free, pure CSS Ours

Left
Live from our directory: Fade In Left. Copy the HTML and CSS →

A practice that makes entrances feel more polished: do not tie opacity and movement to one curve. Our Fade In Left runs two animations on one element, a 0.45 s linear fade and a 0.9 s expo ease-out slide, so the content is fully visible early and the last part of the motion is a pure glide. People can start reading before the element finishes settling.

The same thinking applies to the individual transform properties. Our Move Left to Right animates `translate` and `rotate` as separate properties on separate animations, so the lean can peak when the speed does without one keyframe list fighting the other.

Like every snippet in the directory, the copyable CSS ships with a scoped `prefers-reduced-motion` guard and two custom properties for colour and size.

Watch out for: Several animations on one element are harder to retime as a set. Keep their durations as custom properties so you change them together.

14. Our Rotating Gradient Border: @property, used where it is cheap: Best for animating gradients without moving the element

Platform: Motions directory, free, pure CSS Ours

Rotating border
Live from our directory: Rotating Gradient Border. Copy the HTML and CSS →

Gradients cannot be transitioned directly, but a registered custom property can. Our Rotating Gradient Border registers an angle with `@property`, uses it as the `from` angle of a conic gradient in the border box, and animates only that angle over 4 s on linear timing. The box itself never moves.

This is the one best practice here that knowingly breaks the transform-and-opacity rule: changing a gradient repaints the element every frame. That is acceptable for a small border on one card and a bad idea for a full-screen background. Use it on small areas, and prefer moving a pre-drawn gradient layer with transform when the area is large.

The entry also includes a Tailwind version that spins an oversized gradient layer with `animate-spin` behind the content, which is the transform-based alternative.

Watch out for: Browsers without `@property` support show the gradient frozen. That is a fine fallback for a border, but test it if the motion carries meaning.

How to apply these practices to your own animation

Run any animation through three checks, in this order.

Check the properties

Is it only transform and opacity? If not, can the same look come from fading a pre-drawn layer (Ahlin), from FLIP (Lewis) or from a view transition? If you must animate a paint property like a gradient or shadow, keep the area small and confirm with paint flashing and CPU throttling.

Check the feel

Is it as short as it can be without being jarring (NN/g), eased in the right direction, and interruptible? State changes should be transitions so they reverse cleanly; keyframes are for choreography and loops. Nothing should block a click while it plays.

Check the audience

What happens with prefers-reduced-motion on? Large movement should become a short fade, status animations should slow rather than freeze, and long loops need a pause. Then test on a mid-range phone, not your laptop.

Audit one animation this afternoon

Pick the animation people see most on your site, often a menu, a card hover or a loader. Turn on paint flashing, throttle the CPU, toggle reduced motion and click it twice quickly. If it repaints, drags, jumps or ignores the setting, you now know which practice to apply.

Every snippet in our directory is built to these rules: transform and opacity first, a scoped reduced-motion guard in every copy. For something custom, describe it to our creator and include the constraints from this page in the brief.

A prompt to start from
Make a card hover animation in pure CSS.
On hover: lift 6px with transform and fade in a stronger shadow on a pseudo-element by opacity, never animating box-shadow directly.
Timing: 200 ms ease-out in, 150 ms ease-in out, using transitions so it reverses cleanly.
Under prefers-reduced-motion, keep the shadow fade and drop the lift.

Make your own with AI →

Common questions

What are the best CSS properties to animate?

transform and opacity. web.dev recommends moving with translate, resizing with scale and showing or hiding with opacity, because the browser can usually animate them on the compositor without recalculating layout or repainting. Properties like width, height, top, left and margin trigger layout on every frame.

Should I use will-change on every animation?

No. MDN calls will-change a last resort for existing performance problems. Overusing it wastes memory and can slow the page down. Add it shortly before a specific animation that a profile shows is struggling, and remove it afterwards.

Are CSS animations faster than JavaScript animations?

Often, because CSS animations and the Web Animations API can run on the compositor thread and keep going when JavaScript is busy. But as web.dev notes, if an animation triggers layout or paint, that cost outweighs any CSS versus JavaScript difference. Animating the right properties matters more than the tool.

How long should a CSS animation last?

For most UI changes, 150 to 300 ms. NN/g suggests about 100 ms for simple feedback, 200 to 300 ms for larger changes like a modal, and no more than about 400 ms for large movements. Exits should be a little faster than entrances.

Should I use transitions or keyframe animations?

Use transitions for changes between two states, such as hover, open and closed, or on and off, because they reverse smoothly from wherever they are when interrupted. Use @keyframes for loops, multi-step choreography and animations that play on load.

Sources