Guides · · Maxwell Prehoda
CSS Animation vs Transition: 10 Real UI Cases and Which One Each Needs
A transition animates a property from its old value to its new value when that value changes, so it always needs a trigger: a hover, a checked box, a class or attribute your code toggles. If the state flips back halfway, the transition reverses from wherever it is. An animation runs a @keyframes timeline on its own: it can start on page load, loop forever, pass through as many steps as you like, and restart from 0 percent every time it is applied. The rule of thumb: if the user causes it and can undo it, use a transition. If it runs by itself, loops or has more than two steps, use an animation.
Most comparison pages stop there and were written before 2024. Since then @starting-style, transition-behavior: allow-discrete, scroll-driven animations and interpolate-size have moved several classic cases from one column to the other. Below are ten everyday UI cases, each with the right choice, a live demo from our directory and the browser support as of November 2026 (checked on webstatus.dev and MDN).
1. Hover and focus states: use a transition: Best for link underlines, button fills and card lifts
Platform: CSS transition, supported everywhere
Hover is the textbook transition: two states, a user trigger, and a user who can leave at any moment. With a transition, moving the pointer away halfway through simply reverses the motion from where it is. A keyframe animation in the same spot would either snap back or restart from 0 percent, which is the jumpy hover you see on old sites.
The Underline Grow Link below is the whole pattern in one line that matters: a pseudo-element at scaleX(0) with transition: transform 0.3s ease, set to scaleX(1) on hover. Scaling on X rather than animating width keeps the browser out of layout. The demo is from our directory.
Watch out for: Avoid transition: all. It animates every property that changes, including ones you never meant to, like a width set by a media query. List the properties. And style :focus-visible alongside :hover so keyboard users get the same feedback.
2. Toggles and form controls: use a transition: Best for switches, checkboxes and radio buttons
Platform: CSS transition driven by :checked, supported everywhere
A switch has exactly two states, and the state already lives in the input's :checked pseudo-class, so the CSS only describes the in-between. People also tap switches twice in quick succession, and transitions handle that gracefully because each change starts from the current position.
Our iOS Toggle Switch below transitions the track's background-color over 0.25s and the knob's transform over 0.3s with a custom cubic-bezier. Pressing and holding stretches the knob, which is just one more state to transition to.
Watch out for: Moving the knob with left or margin instead of transform forces layout on every frame. web.dev's rule is to restrict animation to transform and opacity wherever possible.
3. Tooltips, popovers and dialogs: transition with @starting-style: Best for elements that appear, then disappear
Platform: CSS transition plus @starting-style and allow-discrete, Baseline since 2024
This is the case that changed. Transitions used to be useless for things that appear, because they do not run on an element's first style update or when display switches from none to visible. So entry animations needed @keyframes, and exits needed JavaScript to wait before removing the element.
Now @starting-style declares the values to transition from on first render, and transition-behavior: allow-discrete lets display take part in the transition, so the element stays visible until the fade-out finishes. Both have been Baseline since August 2024 (Chrome 117, Firefox 129, Safari 17.5 and 17.4). That makes a transition the better choice for popovers and dialogs: it can be interrupted, and exit comes for free.
Our Classic Tooltip below shows the hover version of the same idea: the bubble fades and rises with a transition on opacity and transform.
Watch out for: Put a standalone @starting-style block after the rule it modifies, because it has the same specificity and the earlier one loses. In older browsers the element simply appears without motion, which is a fine fallback.
4. Accordions and expanding cards: use a transition: Best for revealing content of unknown height
Platform: CSS transition on grid-template-rows today; interpolate-size in Chromium
Open and closed are two states, so this is a transition. The old problem was that you cannot transition height to auto, which led to max-height guesses that made the motion speed wrong. The cross-browser fix today is to transition grid-template-rows from 0fr to 1fr, which lets the browser measure the content for you.
The newer fix is interpolate-size: allow-keywords, which lets height transition straight to auto or max-content. Our Expanding Card Hover below uses the grid-template-rows technique, so it works in every modern browser.
Watch out for: interpolate-size is still limited availability: only Chrome and Edge 129+ support it, per webstatus.dev. Any height animation runs layout on every frame, so keep the content inside light.
5. Loading spinners: use an animation: Best for anything that loops until work finishes
Platform: CSS @keyframes animation, supported everywhere
A spinner has no trigger and no end state. It needs to start the moment it renders and repeat until it is removed, which is exactly what animation-iteration-count: infinite is for. Getting a transition to loop means listening for transitionend in JavaScript and flipping a value back and forth, which is more code for a worse result.
Our Ring Spinner below is one element and one rule: animation: spin 0.8s linear infinite. Linear easing matters here, because any ease-in-out makes the ring visibly pause once per turn.
Watch out for: Do not delete a spinner for people who prefer reduced motion: it tells them the app is working. Slow it down or swap it for a gentle opacity pulse instead. Every snippet in our Loaders directory ships a reduced-motion guard.
6. Skeleton shimmer: use an animation: Best for placeholder screens while data loads
Platform: CSS @keyframes animation, supported everywhere
The shimmer band that sweeps across a skeleton screen is another self-running loop with no user trigger, so it is an animation. Our Skeleton Shimmer below sweeps a gradient by animating background-position from 100 percent to minus 100 percent on a 1.5 second loop.
Keep the sweep slow and soft. Its job is to say that content is coming, not to grab attention.
Watch out for: Animating background-position repaints on every frame. That is fine for one card, but for a long list of skeletons, moving a gradient pseudo-element with transform is cheaper. Our skeleton screen examples compare approaches.
7. Error shake: use an animation: Best for rejecting a wrong password or invalid field
Platform: CSS @keyframes animation added with a class
A shake moves left, right, left and settles in shrinking swings. That is several steps, and a transition only knows two values, so this has to be an animation. It is still triggered by the user, though: your code adds a class when validation fails.
Our Shake X below runs once in 0.8s on a sharp cubic-bezier and holds its end state with fill mode both.
Watch out for: An animation does not replay just because the same class is set again. Remove the class on animationend, then add it on the next failure. And never rely on the shake alone: screen reader users need a real error message tied to the field.
8. Page-load entrances: use an animation: Best for cards and sections fading in as a page loads
Platform: CSS @keyframes animation; @starting-style transition as an alternative
An entrance has no trigger beyond the page itself, so a keyframe animation is the classic tool. Use a fill mode of both or backwards so the element starts invisible instead of flashing in at full opacity first, and stagger siblings with animation-delay: calc(var(--i) * 60ms).
Our Fade In Up below rises about two em over 0.7s on an ease-out quint. If you would rather the entrance be interruptible, the same motion can now be a transition from @starting-style values.
Watch out for: Hiding your main content until an animation runs makes a page feel slower, not faster. Keep entrance distances small and durations short, and do not delay the headline people came to read.
9. Typewriter text: use an animation with steps(): Best for terminal-style hero text and typing effects
Platform: CSS @keyframes with steps() timing, supported everywhere
A typewriter needs two things a transition cannot give you together: the text reveal, stepped one character at a time, and a caret that blinks forever. Both are animations. Our Typewriter Effect below grows the width in ch units with steps(13) and blinks the caret with a step-end infinite loop.
Because the full text is in the DOM from the start, screen readers read it normally while sighted users watch it type.
Watch out for: Stepping width in ch units only lines up exactly with monospace fonts. For proportional fonts, use our Typewriter Any Font version, and set the step count to your text's length.
10. Scroll reveals: use a scroll-driven animation: Best for elements that animate as they enter the viewport
Platform: animation-timeline: view(); Chrome and Edge 115+, Safari 26
Scroll-driven animations tie a @keyframes animation's progress to scroll position instead of time. animation-timeline: view() plays the keyframes as the element crosses the viewport, and animation-range sets where it starts and ends. Transitions cannot do this at all, because scroll position is not a state change on the element. This used to require an IntersectionObserver or a library.
Our Fade In On Scroll below uses view() and also plays once on load, so browsers without support still get an entrance.
Watch out for: Support is recent: Chrome and Edge since 115, Safari since 26 and Firefox since 159 (October 2026).dev. Wrap it in @supports (animation-timeline: view()) and design the fallback first. Large scroll-linked movement can trigger motion sickness, so reduce it under prefers-reduced-motion.
11. Our Keyframe Animations directory: Best for copy-paste @keyframes for entrances, exits and attention
Platform: Motions directory, free; HTML plus CSS Ours
Once you know you need an animation, you rarely need to invent the keyframes. Our Keyframe Animations directory has 90 of them: fades, slides, zooms, flips, bounces, attention seekers like the Heartbeat below, staggered lists and scroll-triggered entrances. Each one has a reduced-motion guard and custom properties for color and size.
For the transition side, the Hover Effects and Toggle Switches directories are almost all transitions.
Watch out for: These are building blocks. Check durations against your product: an entrance tuned to look good in a demo box can feel slow on a page with twenty of them.
12. Our creator: Best for getting either one written to your exact timing
Platform: Motions creator; a full run is free to try Ours
If you would rather describe the motion than decide between transition and animation yourself, the creator writes it. Describe the animation or paste your own component as HTML or a screenshot, answer up to four quick questions while it takes shape, and you get production-ready CSS you can refine in chat.
It is a good way to see both approaches for the same component, because you can ask it in chat to rewrite a keyframe version as a transition, or the other way round, and compare.
Watch out for: Copy, export, save and refine come with the paid plans; the free run lets you see the result first.
How to choose between a CSS transition and an animation
Ask what triggers it
If a user action or a state change starts it (hover, focus, :checked, a class your code toggles, an open attribute), start with a transition. If nothing starts it except the page loading, the element existing or the user scrolling, use an animation. Scroll-linked motion is always an animation with animation-timeline.
Ask how many steps it has
Two values, from A to B, is a transition. Three or more stops, a loop, a bounce that passes through several positions or a stepped typewriter is an animation. If you find yourself chaining transitionend listeners to fake a sequence, you wanted @keyframes, or a library like GSAP; see CSS vs GSAP.
Ask what happens if it is interrupted
Transitions reverse smoothly from wherever they are, which is what menus, toggles and hovers need. Animations restart from 0 percent when re-applied, which is what a spinner or a shake needs. When in doubt, interrupt it yourself: hover on and off quickly, double-tap the toggle. Then tune the duration with our animation duration guide.
Fix one jumpy interaction today
Open your app and hover on and off every button and card quickly. Anything that snaps back or restarts is an animation doing a transition's job, and moving it to a transition on transform and opacity fixes it in a few lines. Then look for anything you hide with JavaScript timeouts on close: @starting-style and allow-discrete can probably replace it.
Start from a snippet in the directory, or describe the interaction and let the creator write it.
Make a popover menu in pure CSS that opens with a transition, not keyframes. On open: fade from 0 to 1 and scale from 0.96 to 1 over 180ms with an ease-out curve, using @starting-style for the entry values. On close: reverse over 120ms and use transition-behavior: allow-discrete so display: none waits for the fade. Animate only opacity and transform, and under prefers-reduced-motion keep the fade but drop the scale.
Common questions
What is the main difference between a CSS transition and a CSS animation?
A transition animates a property between its old and new value when something changes it, so it needs a trigger and has two states. An animation runs a @keyframes timeline on its own, can loop and can have any number of steps.
Which is better for performance, transition or animation?
Neither. Both run on the same browser animation engine. Performance depends on which property you animate: transform and opacity can skip layout and paint, while width, top, left or box-shadow make the browser do more work every frame.
Can a CSS transition loop?
Not on its own. You would have to listen for transitionend in JavaScript and toggle the value back. For anything that repeats, use an animation with animation-iteration-count: infinite.
Can I use a transition and an animation on the same element?
Yes, as long as they animate different properties, for example a looping glow animation plus a transition on transform for hover. Animating the same property with both makes them fight, so split the work across properties or elements.
What does @starting-style do?
It sets the values an element transitions from when it first renders or comes back from display: none. Together with transition-behavior: allow-discrete, it lets plain transitions handle enter and exit animations, which used to need keyframes or JavaScript. It has been Baseline since August 2024.