Smooth CSS Keyframe Animations Without a Library
Build buttery-smooth 60fps keyframe animations leveraging GPU compositing without adding bloated JavaScript runtime dependencies.
CSS Animation Generator
100% In-browser execution. Zero server uploads, instant results, free forever.
- 1. Only Two Properties Animate Without the Main Thread
- 2. Rewriting Layout Animations as Transforms
- 3. will-change: The Hint That Backfires When Overused
- 4. Duration and Easing: The Numbers That Read as Quality
- 5. prefers-reduced-motion Is Not Optional
- 6. Finding the Frame That Drops
- 7. Frequently Asked Questions (FAQ)
Only Two Properties Animate Without the Main Thread
Smooth CSS animation is not about easing curves or clever keyframes. It is about which properties the browser can hand entirely to the compositor.
A browser renders a frame in stages: style, layout, paint, composite. Animating a property that forces an earlier stage means redoing that stage sixty times a second on the main thread, competing with your JavaScript. Animating a property handled at the last stage means the compositor can run it on its own, frequently on the GPU, even while the main thread is busy.
In practice, exactly two properties composite cleanly across every current engine:
transformβ translate, scale, rotate, skewopacity
Everything else has a cost. Animating width, height, top, left, margin or padding triggers layout, the most expensive stage, because the browser must recompute the geometry of the element and potentially everything around it. Animating background-color, box-shadow, border-radius or color triggers paint, which is cheaper than layout but still main-thread work proportional to the pixel area involved.
This is why the same visual effect can be smooth or janky depending purely on how it is expressed. Sliding a panel in with left: -300px β 0 and with transform: translateX(-300px) β none look identical and cost radically different amounts.
Rewriting Layout Animations as Transforms
Most expensive animations have a direct transform equivalent.
Movement. Replace top, left, right, bottom and margin offsets with translate:
```css /* expensive: triggers layout on every frame */ @keyframes slide-in-bad { from { left: -320px; } to { left: 0; } }
/* cheap: composited */ @keyframes slide-in { from { transform: translateX(-320px); } to { transform: translateX(0); } } ```
Size. Replace width and height with scale. The caveat is real: scaling an element scales its text and borders too, so this substitution suits cards, overlays and shapes rather than text containers that must stay crisp.
Appearance and disappearance. Animate opacity, never display. display is not animatable and switching it mid-animation cancels the effect. The correct pattern pairs opacity with visibility, which is animatable in the sense that it snaps at the end of the transition rather than the start:
.panel {
opacity: 0;
visibility: hidden;
transition: opacity 200ms ease, visibility 0s linear 200ms;
}
.panel.is-open {
opacity: 1;
visibility: visible;
transition: opacity 200ms ease, visibility 0s;
}
Height, honestly. Animating an element from zero to its natural height is the one common case with no clean transform equivalent, because the final height is unknown. Modern browsers support interpolating to height: auto with interpolate-size: allow-keywords, and grid-template-rows: 0fr β 1fr is a widely supported alternative. Both still cost layout; the honest position is that this animation is expensive and should be short.
will-change: The Hint That Backfires When Overused
will-change: transform tells the browser to promote an element to its own compositing layer ahead of time, so the first frame of an animation does not stall while that happens.
It is genuinely useful and routinely misused.
Each promoted layer consumes GPU memory proportional to its rasterised size. A handful of promoted elements is unremarkable. Applying will-change to a long list, or leaving it set permanently in a stylesheet, can exhaust memory on mobile devices and produce exactly the stutter it was meant to prevent.
Two rules keep it safe:
- Set it close to the moment it is needed and remove it afterwards. Adding it on hover or focus, or via a class applied just before the animation begins, is the intended use.
- Never apply it to a large number of elements at once.
.card { transition: transform 180ms ease; }
.card:hover { will-change: transform; transform: translateY(-4px); }
The older transform: translateZ(0) and backface-visibility: hidden hacks achieve promotion as a side effect. They still work, but they say nothing about intent and cannot be removed as cleanly. will-change is the property that exists for this and is the one to reach for.
Duration and Easing: The Numbers That Read as Quality
Once an animation is cheap, what remains is whether it feels right, and that is mostly two numbers.
Duration. Interface motion lives in a narrow band. Under roughly 100ms a transition is perceived as an abrupt change rather than a movement. Over roughly 400ms it begins to feel as though the interface is waiting on itself. Useful defaults: 120β180ms for small state changes such as hover and focus, 200β300ms for entrances and exits, and 300β400ms reserved for large surfaces crossing a substantial distance.
Easing. Linear motion looks mechanical because almost nothing in the physical world moves at a constant speed. The defaults encode intent:
ease-outβ fast at the start, settling at the end. Correct for things entering, because the element arrives promptly and comes to rest gently.ease-inβ slow at the start, accelerating away. Correct for things leaving.ease-in-outβ for movement that both begins and ends on screen.
A custom cubic-bΓ©zier is worth it only when you want a specific character. cubic-bezier(0.16, 1, 0.3, 1) produces a decisive, slightly-overshooting settle that suits panels and modals:
.panel { transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1); }
Distance should influence duration. An element crossing 600px in the same 180ms as one moving 4px will look like it teleported. Scaling duration gently with distance β not linearly, but noticeably β is what separates considered motion from uniform motion.
prefers-reduced-motion Is Not Optional
Some people experience nausea, dizziness or migraine from interface motion. Vestibular disorders are common, and the operating system already exposes the preference. Honouring it is a correctness requirement, not a nicety.
The wrong implementation removes all animation, which frequently breaks interfaces that rely on a transition to communicate that something changed. The better approach is to replace movement with a fade and shorten durations, keeping the feedback while removing the travel:
@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;
}
}
That global reset is the widely used safety net, and it is a reasonable floor. Where an animation carries meaning, prefer a targeted rule that keeps an opacity change while dropping the transform:
@media (prefers-reduced-motion: reduce) {
.panel { transition: opacity 120ms ease; transform: none; }
}
Large parallax effects, auto-playing carousels and anything that moves across a significant portion of the viewport are the highest-risk patterns and should be disabled outright under this query.
Finding the Frame That Drops
When an animation stutters, guessing which property is responsible is slower than measuring.
Open the browser's performance profiler, record while the animation runs, and look at the frame timeline. Long purple Layout bars mean a geometry property is being animated β find it and convert it to a transform. Long green Paint bars mean a paint-triggering property such as box-shadow or background-color is animating over a large area. Frames that are mostly yellow Scripting mean the animation is fine and your JavaScript is starving the main thread; in that case moving the animation to the compositor with transform will keep it smooth even while the script runs.
Chromium's rendering panel adds two checkboxes worth knowing. Paint flashing highlights repainted regions in green β a well-built animation should show almost none. Layer borders draws the boundaries of compositing layers, which makes it immediately obvious whether promotion is happening and whether far too many layers exist.
The most common single finding: an animation that is correctly written with transforms but sits inside a container whose box-shadow or filter is also transitioning, forcing a repaint of the whole area every frame regardless.
The CSS animation generator builds keyframes using transform and opacity by default, which is the distinction this article is about. For the surfaces being animated, the gradient studio and glassmorphism generator produce the backgrounds.
Frequently Asked Questions (FAQ)
Which CSS properties are cheapest to animate?
Only transform and opacity can be handled entirely by the compositor, so those two animate without triggering layout or paint on the main thread. Animating width, height, top, left, margin or padding forces a layout recalculation every frame, and animating background-color, box-shadow, border-radius or color forces a repaint. The same visual effect is often available as a transform, and expressing it that way is what makes it smooth.
How do I animate an element sliding in without causing jank?
Use transform: translateX() rather than the left or margin properties. A keyframe going from transform: translateX(-320px) to transform: translateX(0) is composited and does not trigger layout, while the same movement expressed with left recomputes geometry on every frame. Pair it with an ease-out timing function so the element arrives promptly and settles gently.
When should I use will-change?
Apply will-change shortly before an animation starts, typically on hover or focus or through a class added just beforehand, and remove it afterwards. Each promoted element consumes GPU memory proportional to its rasterised size, so leaving it set permanently in a stylesheet or applying it across a long list can exhaust memory on mobile and cause the very stutter it was meant to prevent.
What duration should a UI animation be?
Roughly 120 to 180 milliseconds for small state changes such as hover and focus, 200 to 300 milliseconds for entrances and exits, and 300 to 400 milliseconds only for large surfaces travelling a long distance. Below about 100 milliseconds a transition reads as an abrupt jump rather than movement, and above about 400 milliseconds the interface begins to feel as if it is waiting on itself.
Which easing function should I use?
Use ease-out for elements entering the screen, so they arrive quickly and settle gently. Use ease-in for elements leaving. Use ease-in-out when the movement both starts and ends on screen. Avoid linear, which looks mechanical because very little in the physical world moves at constant speed.
How do I respect prefers-reduced-motion?
Add a prefers-reduced-motion: reduce media query that reduces animation and transition durations to near zero as a global floor, and for animations that carry meaning replace the movement with a short opacity change rather than removing the feedback entirely. Parallax effects, auto-playing carousels and anything travelling across a large part of the viewport should be disabled outright under that query.
Related Guides & Solutions
Frosted Glass UI in Pure CSS (Glassmorphism)
Neumorphism in CSS: Soft UI Shadows That Work
Ready to use CSS Animation Generator?
Execute this workflow privately on your device right now without creating an account or paying for cloud API credits.