CSS Animation Guide: Transitions, Keyframes, and Performance
CSS animations bring interfaces to life, providing visual feedback, guiding user attention, and creating memorable experiences. When done well, animations feel natural and intuitive. When done poorly, they cause jank, distract users, and even trigger physical discomfort. This guide covers everything from the fundamentals of transitions and keyframes to advanced performance optimization and accessibility, giving you the knowledge to create animations that are smooth, purposeful, and inclusive.
CSS Transitions vs Animations
CSS provides two distinct mechanisms for creating motion: transitions and animations. Understanding when to use each is the foundation of effective CSS animation work.
CSS Transitions
Transitions animate the change between two states of a CSS property. You define which properties to animate, how long the animation takes, and what easing curve to use. The browser handles the interpolation between the start and end values automatically. Transitions are triggered by state changes like hover, focus, class additions, or JavaScript property changes.
.button {
background: #3b82f6;
transform: scale(1);
transition: background 0.3s ease, transform 0.2s ease;
}
.button:hover {
background: #2563eb;
transform: scale(1.05);
}
Transitions are ideal for simple, two-state animations. They require minimal code, are easy to understand, and work reliably across browsers. The limitation is that they only animate between two values �?you cannot define intermediate steps.
CSS Animations (Keyframes)
Animations use @keyframes rules to define sequences of
style changes across any number of steps. Unlike transitions,
animations can run independently of state changes, loop indefinitely,
play in reverse, and pause mid-animation. They give you precise
control over every frame of the animation.
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.pulse-element {
animation: pulse 2s ease-in-out infinite;
}
When to Use Each
| Scenario | Use Transition | Use Animation |
|---|---|---|
| Hover effects | Yes | No |
| Focus states | Yes | No |
| Toggle visibility | Yes | Possible |
| Multi-step sequences | No | Yes |
| Looping animations | No | Yes |
| Independent of state | No | Yes |
| Entrance animations | Possible | Yes |
Keyframe Syntax Deep Dive
The @keyframes rule is the heart of CSS animations.
Understanding its syntax fully unlocks the power of multi-step
animation sequences.
Percentage-Based Keyframes
Keyframes are defined using percentage values from 0% to 100%, representing the progression through the animation timeline. You can define as many keyframes as needed, and the browser interpolates between them:
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
25% {
transform: translateY(-20px);
}
50% {
transform: translateY(0);
}
75% {
transform: translateY(-10px);
}
}
Multiple keyframes can share the same style block by comma-separating
the percentage values, as shown with 0%, 100% in the
example above. This is useful for animations that return to their
starting state.
The Animation Shorthand
The animation shorthand property combines eight
sub-properties into a single declaration. The order is: name,
duration, timing-function, delay, iteration-count, direction,
fill-mode, and play-state.
.element {
animation: slideIn 0.5s ease-out 0.2s 1 forwards running;
}
/* Equivalent longhand */
.element {
animation-name: slideIn;
animation-duration: 0.5s;
animation-timing-function: ease-out;
animation-delay: 0.2s;
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
animation-play-state: running;
}
Animation Fill Mode
The animation-fill-mode property controls what styles
apply before and after the animation runs. This is one of the most
commonly misunderstood animation properties:
- none (default): The element returns to its pre-animation styles when the animation ends.
- forwards: The element retains the styles of the last keyframe after the animation ends. Essential for entrance animations where you want the element to stay in its final position.
- backwards: The element applies the first keyframe styles during the animation delay period, before the animation starts.
- both: Combines forwards and backwards behavior. The element shows the first keyframe during the delay and retains the last keyframe after completion.
Easing Functions
Easing functions (also called timing functions) define the acceleration curve of an animation. They determine whether the animation starts slowly and speeds up, starts quickly and slows down, or follows a more complex pattern. The right easing function makes animations feel natural; the wrong one makes them feel robotic or jarring.
Built-In Easing Functions
- linear: Constant speed throughout the animation. Rarely looks natural because objects in the real world do not move at constant speed. Useful for mechanical or rotating animations.
- ease (default): Starts slowly, accelerates in the middle, and slows down at the end. A good general-purpose easing that works for most UI transitions.
- ease-in: Starts slowly and accelerates. Creates a "building momentum" feel. Good for elements leaving the screen or falling.
- ease-out: Starts quickly and decelerates. Creates a "settling into place" feel. The most commonly used easing for UI elements entering the screen.
- ease-in-out: Slow start and slow end with faster middle. Similar to ease but with more pronounced acceleration and deceleration. Good for larger, more dramatic transitions.
Cubic Bezier Curves
The cubic-bezier() function lets you define custom easing
curves with precise control. It takes four parameters representing the
control points of a cubic Bezier curve:
cubic-bezier(x1, y1, x2, y2). The x values represent time
(0 to 1) and the y values represent progress (can exceed 0-1 for
overshoot).
.element {
/* Smooth deceleration - great for entrances */
transition: transform 0.3s cubic-bezier(0.0, 0.0, 0.2, 1.0);
/* Smooth acceleration - great for exits */
transition: transform 0.2s cubic-bezier(0.4, 0.0, 1, 1);
/* Material Design standard easing */
transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
}
Step Functions
The steps() function creates discrete, frame-by-frame
animation instead of smooth interpolation. This is useful for sprite
sheet animations or creating a "typing" effect:
@keyframes typing {
from { width: 0; }
to { width: 100%; }
}
.typing-effect {
animation: typing 3s steps(40, end);
overflow: hidden;
white-space: nowrap;
}
Performance Optimization
Smooth animations require hitting 60 frames per second (fps), which means each frame must complete in under 16.67 milliseconds. Failing to meet this budget causes visible jank �?stuttering, dropped frames, and a degraded user experience. Understanding the browser rendering pipeline is key to writing performant animations.
The Rendering Pipeline
When a CSS property changes, the browser goes through up to three stages to update the display:
- Style: Determine which CSS rules apply to which elements.
- Layout: Calculate the size and position of every element. Triggered by changes to width, height, margin, padding, top, left, font-size, and other geometric properties.
- Paint: Fill in the pixels for each element. Triggered by changes to color, background, box-shadow, border-radius, and other visual properties.
- Composite: Combine the painted layers in the correct order. Only this step runs on the GPU. Triggered by changes to transform and opacity.
Animations that only trigger the composite step are the most
performant because they skip layout and paint entirely. This is why
transform and opacity are the gold standard
for performant CSS animations.
Compositor-Only Properties
| Property | Triggers Layout? | Triggers Paint? | GPU Composited? |
|---|---|---|---|
| transform | No | No | Yes |
| opacity | No | No | Yes |
| filter | No | Yes | Sometimes |
| top / left | Yes | Yes | No |
| width / height | Yes | Yes | No |
| margin / padding | Yes | Yes | No |
| background-color | No | Yes | No |
| box-shadow | No | Yes | No |
Using will-change
The will-change property tells the browser which
properties are expected to change, allowing it to optimize ahead of
time by promoting the element to its own compositor layer. However, it
should be used sparingly:
/* Apply will-change just before the animation */
.element:hover {
will-change: transform;
}
/* Remove it after the animation completes */
.element {
transition: transform 0.3s ease;
}
Common mistakes with will-change include applying it to
too many elements (each compositor layer consumes GPU memory) and
leaving it on permanently (wastes resources). A good practice is to
add will-change via JavaScript just before the animation
starts and remove it when the animation ends.
Transform Instead of Layout Properties
The single most impactful performance optimization is replacing layout-triggering properties with transforms:
-
Use
transform: translateX(100px)instead ofleft: 100px -
Use
transform: translateY(50px)instead oftop: 50px -
Use
transform: scale(1.5)instead ofwidth: 150%; height: 150% -
Use
transform: rotate(45deg)instead of rotating via layout
transform or opacity, always
prefer those over properties that trigger layout or paint. This single
principle eliminates the majority of animation performance problems.
Common Animation Patterns
Certain animation patterns appear repeatedly in web interfaces. Mastering these patterns gives you a toolkit for most common UI animation needs.
1. Fade In
The simplest and most universally applicable entrance animation. Elements transition from transparent to opaque:
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 0.4s ease-out forwards;
}
2. Slide Up and Fade
A more dynamic entrance that combines vertical movement with opacity. This creates a sense of the element arriving from below:
@keyframes slideUpFade {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.slide-up {
animation: slideUpFade 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
3. Scale on Hover
A subtle scale increase on hover provides satisfying interactive feedback:
.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-4px) scale(1.02);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}
4. Staggered List Animation
When a list of items appears, staggering the animation delay for each item creates a cascading effect that guides the eye:
.list-item {
opacity: 0;
transform: translateY(10px);
animation: slideUpFade 0.3s ease-out forwards;
}
.list-item:nth-child(1) { animation-delay: 0.0s; }
.list-item:nth-child(2) { animation-delay: 0.05s; }
.list-item:nth-child(3) { animation-delay: 0.1s; }
.list-item:nth-child(4) { animation-delay: 0.15s; }
.list-item:nth-child(5) { animation-delay: 0.2s; }
For dynamic lists, use JavaScript to set the
animation-delay based on each item's index, or use CSS
custom properties:
style="animation-delay: calc(var(--i) * 0.05s)".
5. Loading Spinner
A smooth, continuous rotation is the classic loading indicator:
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid rgba(255, 255, 255, 0.2);
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
Animation Accessibility: prefers-reduced-motion
Not all users benefit from animations. For people with vestibular
disorders, motion sensitivity, or certain cognitive conditions,
animations can cause dizziness, nausea, or seizures. The
prefers-reduced-motion media query allows you to respect
the user's system-level preference for reduced motion.
Implementing Reduced Motion
The recommended approach is to make animations opt-in for users who have not set a reduced motion preference:
/* Default: no animation for users who prefer reduced motion */
.element {
opacity: 1;
transform: none;
}
/* Only animate for users who have no motion preference */
@media (prefers-reduced-motion: no-preference) {
.element {
animation: slideUpFade 0.5s ease-out forwards;
}
}
Alternatively, you can disable specific animations for users who prefer reduced motion:
.element {
animation: slideUpFade 0.5s ease-out forwards;
}
@media (prefers-reduced-motion: reduce) {
.element {
animation: none;
opacity: 1;
transform: none;
}
/* Keep functional transitions but shorten duration */
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
What to Keep and What to Remove
Not all motion should be removed when the user prefers reduced motion. Functional animations that convey meaning or state should be preserved, just simplified:
- Remove: Decorative animations, parallax scrolling, auto-playing carousels, pulsing effects, and looping background animations.
- Simplify: Transition durations should be shortened to near-instantaneous (0.01ms) rather than removed entirely, so state changes are still perceptible.
- Keep: Functional state transitions (hover, focus, active states), progress indicators, and any animation that conveys essential information.
Animation Libraries
While CSS animations handle most UI needs, JavaScript animation libraries provide advanced capabilities like physics-based motion, scroll-linked animations, and orchestration of complex sequences.
CSS-First Libraries
- Animate.css: A collection of pre-built CSS animation classes. Simply add a class name to an element to apply an animation. Good for quick prototyping but can lead to generic-feeling motion if overused.
- Hamburgers: A collection of animated hamburger menu icon transitions, all implemented in pure CSS.
JavaScript Animation Libraries
- Framer Motion: A React animation library that provides declarative animation APIs with spring physics, layout animations, and gesture support. The best choice for React applications.
- GSAP (GreenSock): The most powerful JavaScript animation library. Provides precise timeline control, complex easing, SVG morphing, and scroll-triggered animations. Used by major brands for marketing sites and interactive experiences.
- Anime.js: A lightweight JavaScript animation library with a clean API. Supports CSS properties, SVG, DOM attributes, and JavaScript objects. A good middle ground between CSS-only and GSAP.
- Motion One: A modern, lightweight animation library built on the Web Animations API. Provides a minimal API with spring physics and timeline support.
When to Use JavaScript vs CSS
Use CSS for simple, declarative animations that respond to state changes. Use JavaScript when you need: physics-based motion (springs, momentum), scroll-linked animations, complex sequencing with precise timing control, dynamic animation parameters calculated at runtime, or animations that need to be paused, reversed, or scrubbed programmatically.
Debugging Animations
Animation bugs can be subtle and difficult to diagnose. The right debugging tools and techniques make the process much more efficient.
Chrome DevTools Animations Panel
Chrome DevTools includes a dedicated Animations panel that provides powerful debugging capabilities:
- Timeline view: See all active animations on the page with their durations, delays, and keyframes.
- Playback controls: Slow animations down to 1/4 speed, pause, or scrub through the timeline frame by frame.
- Property inspection: Click on an animation in the timeline to see the exact CSS properties being animated and their values at each keyframe.
- Live editing: Modify animation duration, delay, and easing in real time without reloading the page.
Performance Profiling
Use the Performance panel in Chrome DevTools to identify animation jank:
- Record a performance trace while the animation is running
- Look for frames that exceed the 16.67ms budget (shown as red bars in the frame chart)
- Check for "Layout" operations during animation frames, which indicate layout-triggering properties
- Use the "Rendering" tab to enable "Paint flashing" (shows repainted areas in green) and "Layer borders" (shows compositor layers)
Common Debugging Scenarios
-
Animation not running: Check that
animation-fill-modeis set correctly. Withoutforwardsorboth, the element snaps back to its pre-animation state. Also verify that the element has the correct dimensions �?transform: scale(0)makes the element invisible but still present. -
Janky animation: Open the Performance panel and
look for long frames. Check if you are animating layout-triggering
properties. Switch to
transformandopacity. -
Animation delay feels wrong: Remember that
animation-delaycreates a gap before the animation starts. During this gap, the element shows its pre-animation state unlessanimation-fill-mode: backwardsorbothis set. -
Animation works in Chrome but not Safari: Safari
has stricter parsing for the
animationshorthand. Ensure all values are in the correct order and that you are not using unsupported features.
Working with CSS? Try our free CSS tools to format, minify, and optimize your stylesheets for production.
CSS Formatter CSS MinifierFrequently Asked Questions
Should I use CSS transitions or CSS animations?
Use CSS transitions for simple state changes that go from point A to point B, such as hover effects, focus states, and toggling visibility. Use CSS animations (keyframes) for complex, multi-step sequences, looping animations, or animations that need to run independently of state changes. Transitions are simpler to write; animations offer more control.
What CSS properties are safe to animate for performance?
The most performant properties to animate are transform and opacity, because they can be handled entirely by the GPU compositor without triggering layout or paint. Other properties like width, height, margin, and top trigger layout recalculation, which is expensive. Always prefer transform: translateX() over left, and transform: scale() over width/height changes.
How do I make animations accessible for users with motion sensitivity?
Use the prefers-reduced-motion media query to disable or simplify animations for users who have enabled the 'reduce motion' setting in their operating system. Wrap non-essential animations in @media (prefers-reduced-motion: no-preference) and provide instant state changes as the fallback for users who prefer reduced motion.
What is will-change and when should I use it?
will-change is a CSS property that hints to the browser about which properties will change in the future, allowing it to optimize ahead of time. Use it sparingly and only when you observe performance issues. Apply it just before the animation starts and remove it after the animation ends. Overusing will-change wastes GPU memory and can actually hurt performance.
How do I debug CSS animations?
Chrome DevTools provides powerful animation debugging tools. Open the Animations panel (in the More Tools menu) to see all active animations, scrub through timelines, modify durations and delays in real time, and slow animations down to 1/4 speed. You can also use the Performance panel to identify animation jank and layout thrashing.