CSS Animations: From Simple Transitions to Complex Keyframe Sequences
Master CSS transitions, @keyframes animations, cubic-bezier timing functions, and GPU-accelerated transforms for smooth 60fps web animations.
CSS Animations: From Simple Transitions to Complex Keyframe Sequences
Motion on the web used to mean Flash plugins and jQuery's .animate() method. Today, CSS alone can produce fluid, performant animations that run at 60 frames per second, offloaded entirely to the browser's compositor thread. Understanding how CSS animations and transitions work — and, just as importantly, understanding the rendering pipeline they interact with — is the difference between an interface that feels alive and one that stutters and drains batteries. This guide walks through the entire landscape, from a single transition declaration to multi-step keyframe sequences, GPU acceleration, and the emerging Web Animations API.
The CSS Transition Property
A CSS transition is the simplest form of animation. You declare which property to animate, how long the animation should take, and optionally which timing function to use and how long to wait before starting. When the specified property changes — because of a hover, a class toggle, a media query shift, or any other state change — the browser interpolates smoothly between the old and new values instead of snapping instantly.
The shorthand syntax packs everything into one line: transition: opacity 300ms ease-in-out 50ms means "animate the opacity property over 300 milliseconds using an ease-in-out curve, but wait 50 milliseconds before starting." You can comma-separate multiple properties, or use the keyword all to transition every animatable property at once. The all keyword is convenient for prototyping but should be used cautiously in production — it can trigger transitions on properties you did not intend to animate, and it forces the browser to watch every computed style for changes.
Transitions are inherently two-state affairs. They animate from value A to value B and back again. There is no concept of waypoints, loops, or multi-step choreography. When you need more than two states, you need keyframe animations. But for the vast majority of interactive feedback — hover effects, focus rings, accordion expansions, opacity fades — transitions are the right tool. They require less code, are easier to reason about, and degrade gracefully (if a browser does not support a transition, the property simply changes instantly).
Cubic-Bézier Curves and Timing Functions
The timing function controls the acceleration curve of an animation. CSS defines several keyword presets: linear produces constant speed, ease starts slowly, accelerates through the middle, and decelerates at the end, ease-in starts slowly and accelerates, ease-out decelerates toward the end, and ease-in-out combines both. Under the hood, each of these keywords maps to a specific cubic Bézier curve defined by two control points.
A cubic Bézier curve in CSS is expressed as cubic-bezier(x1, y1, x2, y2), where each control point sits in a coordinate space where the x-axis represents time (0 to 1) and the y-axis represents progress (0 to 1). The curve always starts at (0, 0) and ends at (1, 1), so you are really defining the shape of the path between those two anchors. The keyword ease, for instance, is equivalent to cubic-bezier(0.25, 0.1, 0.25, 1.0). By adjusting the control points you can create overshoot effects (y values greater than 1), anticipation (y values briefly going negative), or snappy spring-like motions that give interfaces a tactile, physical quality.
Designing custom curves by hand can be unintuitive, which is why visual tools are invaluable. Loopaloo's CSS Animation Generator lets you drag control points and see the resulting motion in real time, then copy the CSS directly into your stylesheet. Experimenting with different curves is one of the fastest ways to develop an intuition for how timing functions shape the feel of an interface.
The @keyframes Rule
When you need more than a simple A-to-B interpolation, CSS keyframe animations take over. A @keyframes rule defines a named animation as a series of waypoints, each specified by a percentage of the total animation duration. The browser interpolates between successive waypoints, effectively creating a multi-step animation timeline.
A basic example might define three waypoints: at 0% the element is fully transparent and shifted 20 pixels down, at 60% it is fully opaque and has overshot its final position by 5 pixels, and at 100% it is opaque and in its resting position. The percentages give you fine-grained control over the pacing of each segment independently. You can also use the keywords from and to as aliases for 0% and 100%, though percentages are more common in practice because most useful animations have intermediate steps.
Keyframe animations are applied to elements using the animation shorthand property, which accepts the animation name, duration, timing function, delay, iteration count, direction, fill mode, and play state. Each of these sub-properties fine-tunes the behavior. The animation-iteration-count can be a number or the keyword infinite for looping animations like spinners and pulsing indicators. The animation-direction property controls whether the animation plays forward, backward, or alternates between the two on successive iterations — the alternate value is particularly useful for creating ping-pong effects without defining reverse keyframes manually.
The animation-fill-mode property deserves special attention because it is a common source of confusion. By default, an animation has no effect on the element before it starts or after it ends — the element snaps back to its original styles. Setting fill-mode: forwards retains the styles from the final keyframe after the animation completes. Setting fill-mode: backwards applies the styles from the first keyframe during the delay period before the animation starts. And fill-mode: both combines both behaviors. Forgetting to set a fill mode is one of the most frequent reasons developers see an element "flash" back to its original position at the end of an animation.
GPU Acceleration and the Rendering Pipeline
Not all CSS properties are created equal when it comes to animation performance. To understand why, you need a basic mental model of the browser's rendering pipeline. When styles change, the browser goes through up to four phases: style recalculation (figuring out which rules apply), layout (computing the size and position of every element), paint (filling in pixels for each element), and compositing (layering painted textures into the final image). These phases form a waterfall — triggering layout forces a repaint, which forces recompositing.
Properties like width, height, margin, padding, top, and left trigger layout when they change. Layout is the most expensive phase because changing one element's size can cascade into repositioning many other elements. Properties like background-color, box-shadow, and border-radius skip layout but still trigger paint, which is moderately expensive. The golden properties for animation are transform and opacity, which skip both layout and paint entirely and are handled solely in the compositing phase. The compositor runs on the GPU, on a separate thread from the main JavaScript thread, which means transform and opacity animations can run at a smooth 60 frames per second even while JavaScript is busy doing heavy computation.
This is why experienced developers reach for transform: translateX() instead of animating left, and transform: scale() instead of animating width. The visual result is often identical, but the performance characteristics are dramatically different. A complex layout recalculation might take 15 milliseconds — nearly the entire budget for a single frame at 60fps — while a composited transform takes microseconds.
The will-change property lets you hint to the browser that a specific property is about to be animated, giving it the opportunity to promote the element to its own compositor layer in advance. Writing will-change: transform before an animation starts can eliminate the first-frame jank that sometimes occurs when the browser promotes a layer mid-animation. However, will-change should be used sparingly and removed when the animation is done. Every promoted layer consumes GPU memory, and over-promoting elements can actually degrade performance on memory-constrained devices. Think of will-change as a surgical tool, not a global optimization.
requestAnimationFrame vs CSS Animations
JavaScript-based animation through requestAnimationFrame and CSS-based animation serve different purposes, and choosing the wrong one leads to unnecessary complexity or compromised performance. CSS animations are ideal when the animation is declarative and predetermined — you know the start state, the end state, and the timing in advance. Loading spinners, page transitions, hover effects, and scroll-triggered fade-ins are all natural fits for CSS.
requestAnimationFrame is the right choice when the animation depends on runtime values that change continuously — following the mouse cursor, reacting to physics simulations, updating a canvas, or coordinating complex choreography that depends on user input. It gives you per-frame control over the animation loop and integrates naturally with application state. The key advantage of requestAnimationFrame over older approaches like setInterval is that the browser synchronizes callbacks with the display's refresh rate and pauses them when the tab is not visible, saving CPU and battery.
In many real-world interfaces, the two approaches coexist. A scroll-triggered animation might use an Intersection Observer (in JavaScript) to add a CSS class that triggers a CSS keyframe animation. The JavaScript handles the detection logic; the CSS handles the motion. This division of labor keeps the animation itself on the compositor thread while letting JavaScript orchestrate when it starts.
The Web Animations API
The Web Animations API (WAAPI) bridges the gap between CSS animations and JavaScript control. It lets you create and control animations programmatically using the same underlying engine that powers CSS animations and transitions. You call element.animate(keyframes, options) and get back an Animation object with methods like play(), pause(), reverse(), and finish(), plus a finished promise that resolves when the animation completes.
WAAPI gives you the compositor-thread performance of CSS animations with the dynamic control of JavaScript. You can change an animation's playback rate on the fly, seek to a specific point in time, or smoothly reverse a partially completed animation — things that are awkward or impossible with pure CSS class toggling. Browser support is now excellent across modern browsers, making WAAPI a practical choice for production applications.
Respecting Motion Preferences
Not everyone experiences motion the same way. For people with vestibular disorders, large or sudden animations can cause dizziness, nausea, or disorientation. The prefers-reduced-motion media query lets you detect when a user has enabled reduced-motion settings in their operating system and adapt your animations accordingly.
The responsible pattern is to wrap non-essential animations in a media query that checks for the absence of a reduced-motion preference: @media (prefers-reduced-motion: no-preference) { ... }. This means the default experience is no animation, and motion is added only for users who have not opted out. Alternatively, you can define your full animations normally and then override them inside a @media (prefers-reduced-motion: reduce) block, either removing them entirely or replacing them with subtler alternatives like simple opacity fades. Either approach is valid; the important thing is that you have considered the issue at all, and that essential functionality never depends on animation to be usable.
Practical Animation Patterns
With the theory in place, here are some patterns that appear constantly in modern interfaces and that illustrate the principles discussed above.
Loading spinners are the canonical infinite CSS animation. A @keyframes spin rule rotates an element from 0 to 360 degrees, applied with animation: spin 1s linear infinite. The linear timing function is critical — an easing curve would make the spinner appear to stutter on each revolution. The rotation is a transform, so it runs entirely on the GPU compositor.
Page transitions, especially in single-page applications, typically combine an opacity fade with a subtle vertical translate. The entering page fades in and slides up from a slight offset, while the exiting page fades out. Using animation-fill-mode: both ensures the entering page starts invisible (during its delay) and the exiting page stays invisible after it finishes. Coordinating the timing so that the exit animation completes before the enter animation begins avoids having both pages visible simultaneously.
Hover effects benefit enormously from transitions rather than keyframe animations. A button that darkens its background color on hover, for instance, needs only transition: background-color 150ms ease-out on its resting state. The timing should be short — 100 to 200 milliseconds — because hover feedback needs to feel immediate. Longer transitions make interfaces feel sluggish and unresponsive.
Scroll-triggered animations, where elements fade in or slide into view as the user scrolls down the page, are best implemented using the Intersection Observer API to add a CSS class when an element enters the viewport. The actual animation lives in CSS, triggered by the presence of the class. This pattern avoids the performance pitfalls of scroll event listeners and keeps the motion on the compositor thread.
Gradient animations add visual richness but require care. Animating the background property directly is expensive because gradients trigger repaints. A common workaround is to create an oversized gradient background and animate its position using background-position or, better yet, animate a pseudo-element's transform. Loopaloo's Gradient Generator can help you design the gradient itself before you begin animating it.
The landscape of CSS animation is broad and continually evolving. Newer features like scroll-driven animations (animation-timeline: scroll()), view transitions (the View Transition API), and individual transform properties (translate, rotate, scale as separate CSS properties rather than functions inside transform) are expanding what is possible without JavaScript. The fundamentals, however, remain constant: understand the rendering pipeline, animate only compositor-friendly properties when performance matters, respect user preferences, and choose the simplest tool that gets the job done. Transitions for two-state changes, keyframes for multi-step sequences, and the Web Animations API when you need JavaScript control over compositor-thread performance. Master those three layers and you can make any interface move.
Related Tools
CSS Animation Generator
Create custom CSS keyframe animations visually. Add keyframes, adjust timing, and preview animations in real-time.
CSS Gradient Generator
Create stunning CSS gradients with linear/radial/conic types, angle control, position control, multiple color stops, 11 presets, and reverse/randomize features
Related Articles
Try Our Free Tools
200+ browser-based tools for developers and creators. No uploads, complete privacy.
Explore All Tools