1. Introduction: Why CSS Animations Matter for UX
In 2026, CSS animations have become an indispensable tool in the modern web developer's arsenal. They're no longer just decorative flourishes—they're fundamental to creating intuitive, engaging user experiences that guide attention, provide feedback, and make interfaces feel alive.
Why animations matter:
- User Feedback: Animations confirm actions, like a button press or form submission, making interfaces feel responsive.
- Visual Continuity: Smooth transitions between states help users understand how elements relate and where content comes from.
- Attention Direction: Motion naturally draws the eye, helping you guide users to important information or actions.
- Perceived Performance: Well-crafted loading animations and skeleton screens make wait times feel shorter.
- Brand Personality: Unique animation styles can differentiate your product and create memorable experiences.
CSS animations offer several advantages over JavaScript-based alternatives: they're declarative, performant (leveraging GPU acceleration), and work even when JavaScript is disabled or still loading. They're also easier to maintain and debug than complex animation libraries.
This guide will take you from CSS animation fundamentals through advanced techniques, performance optimization, and modern features that are shaping web animation in 2026.
Need to Create Animations Quickly?
Try our CSS Animation Generator to create custom keyframe animations with a visual editor. Generate production-ready code in seconds!
Try CSS Animation Generator2. CSS Transitions vs Animations
CSS offers two primary mechanisms for creating motion: transitions and animations. Understanding when to use each is crucial for writing efficient, maintainable code.
CSS Transitions
Transitions are ideal for simple state changes. They animate between two states when triggered by a state change (like :hover, :focus, or a class toggle).
Best for:
- Hover effects on buttons and links
- Focus states on form inputs
- Simple toggles (accordion expansion, dropdown menus)
- Any two-state change that needs smoothing
/* Simple button hover transition */
.button {
background-color: #3498db;
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
transition: all 0.3s ease;
}
.button:hover {
background-color: #2980b9;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
CSS Animations (@keyframes)
Animations use @keyframes to define multiple intermediate steps, offering fine-grained control over complex motion sequences. They can loop, reverse, and run automatically without user interaction.
Best for:
- Loading spinners and progress indicators
- Attention-grabbing effects (pulse, shake, bounce)
- Complex multi-step sequences
- Continuous or looping animations
- Animations that run on page load
/* Loading spinner animation */
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
Key Differences
| Feature | Transitions | Animations |
|---|---|---|
| Trigger | State change required | Can run automatically |
| Steps | Two states (start/end) | Multiple keyframes |
| Looping | No built-in looping | iteration-count (infinite) |
| Control | Limited | Fine-grained (play-state, direction, etc.) |
| Complexity | Simple, concise | More verbose, powerful |
3. CSS Transition Properties
CSS transitions are controlled by four sub-properties, which can be combined using the transition shorthand.
transition-property
Specifies which CSS properties should be transitioned. You can target specific properties or use all to transition everything.
/* Transition specific properties */
.card {
transition-property: transform, opacity, box-shadow;
}
/* Transition all animatable properties */
.button {
transition-property: all;
}
Performance Warning
While transition: all is convenient, it can cause performance issues by transitioning properties you didn't intend to animate. Be explicit when possible, especially for properties that trigger layout or paint (like width, height, top, left).
transition-duration
Controls how long the transition takes. Specified in seconds (s) or milliseconds (ms).
.fast-transition {
transition-duration: 0.15s; /* 150ms - snappy */
}
.medium-transition {
transition-duration: 0.3s; /* 300ms - balanced */
}
.slow-transition {
transition-duration: 0.6s; /* 600ms - dramatic */
}
Duration guidelines:
- 0.1-0.2s: Instant feedback (hover effects, button presses)
- 0.2-0.4s: General-purpose transitions (most UI changes)
- 0.4-0.6s: Attention-drawing (modals, large movements)
- 0.6s+: Dramatic effects (use sparingly)
transition-timing-function
Defines the acceleration curve—how the transition progresses over time.
.ease {
transition-timing-function: ease; /* Default: slow start, fast middle, slow end */
}
.linear {
transition-timing-function: linear; /* Constant speed */
}
.ease-in {
transition-timing-function: ease-in; /* Slow start, accelerate */
}
.ease-out {
transition-timing-function: ease-out; /* Fast start, decelerate */
}
.ease-in-out {
transition-timing-function: ease-in-out; /* Smooth acceleration and deceleration */
}
transition-delay
Adds a pause before the transition starts. Useful for sequencing multiple animations.
/* Staggered animation effect */
.item:nth-child(1) { transition-delay: 0s; }
.item:nth-child(2) { transition-delay: 0.1s; }
.item:nth-child(3) { transition-delay: 0.2s; }
.item:nth-child(4) { transition-delay: 0.3s; }
Shorthand Syntax
Combine all four properties in the transition shorthand:
/* transition: property duration timing-function delay */
.element {
transition: transform 0.3s ease-out 0.1s;
}
/* Multiple transitions */
.multi {
transition:
transform 0.3s ease-out,
opacity 0.2s ease-in,
background-color 0.4s linear;
}
4. CSS @keyframes Basics
The @keyframes rule is where you define the animation sequence. Think of it as a timeline where you specify what should happen at different points during the animation.
Basic Syntax
@keyframes animationName {
0% {
/* Starting state */
opacity: 0;
transform: translateY(20px);
}
100% {
/* Ending state */
opacity: 1;
transform: translateY(0);
}
}
Percentage-Based Keyframes
You can define as many intermediate states as needed using percentages:
@keyframes complexAnimation {
0% {
transform: scale(1);
background-color: #3498db;
}
25% {
transform: scale(1.2);
background-color: #9b59b6;
}
50% {
transform: scale(1) rotate(180deg);
background-color: #e74c3c;
}
75% {
transform: scale(1.2) rotate(180deg);
background-color: #f39c12;
}
100% {
transform: scale(1) rotate(360deg);
background-color: #3498db;
}
}
from/to Shorthand
For simple two-state animations, you can use from and to instead of 0% and 100%:
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
Applying Keyframe Animations
Once you've defined your keyframes, apply them to elements using the animation property:
.fade-in-element {
animation: fadeIn 0.5s ease-out;
}
.complex-element {
animation: complexAnimation 3s ease-in-out infinite;
}
Multiple Keyframe Selectors
You can combine multiple percentages when they share the same styles:
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
5. Animation Properties Deep Dive
CSS animations are controlled by eight individual properties, all combinable in the animation shorthand. Let's explore each one in detail.
animation-name
Specifies the name of the @keyframes rule to apply:
.element {
animation-name: slideIn;
}
animation-duration
How long the animation takes to complete one cycle:
.element {
animation-duration: 2s; /* 2 seconds */
}
animation-timing-function
Controls the pacing of the animation (covered in detail in the next section):
.element {
animation-timing-function: ease-in-out;
}
animation-delay
Wait time before the animation starts:
.element {
animation-delay: 0.5s; /* Starts after 500ms */
}
/* Negative delays start partway through */
.instant-start {
animation-delay: -1s; /* Starts 1s into animation */
}
animation-iteration-count
How many times the animation repeats:
.once {
animation-iteration-count: 1; /* Default: plays once */
}
.three-times {
animation-iteration-count: 3;
}
.infinite {
animation-iteration-count: infinite; /* Loops forever */
}
animation-direction
Controls whether the animation runs forward, backward, or alternates:
.normal {
animation-direction: normal; /* 0% → 100% (default) */
}
.reverse {
animation-direction: reverse; /* 100% → 0% */
}
.alternate {
animation-direction: alternate; /* Forward, then backward, repeat */
}
.alternate-reverse {
animation-direction: alternate-reverse; /* Backward, then forward, repeat */
}
animation-fill-mode
Determines how styles are applied before and after the animation:
.none {
animation-fill-mode: none; /* No styles applied outside animation */
}
.forwards {
animation-fill-mode: forwards; /* Retains final keyframe styles */
}
.backwards {
animation-fill-mode: backwards; /* Applies first keyframe during delay */
}
.both {
animation-fill-mode: both; /* Combines forwards + backwards */
}
fill-mode Example
If you have a fade-in animation with a 1s delay, fill-mode: backwards will make the element invisible during the delay (applying the 0% opacity from the keyframes). fill-mode: forwards will keep the element at full opacity after the animation completes.
animation-play-state
Pauses or resumes the animation:
.running {
animation-play-state: running; /* Default */
}
.paused {
animation-play-state: paused;
}
/* Pause on hover */
.carousel {
animation: slide 10s linear infinite;
}
.carousel:hover {
animation-play-state: paused;
}
Animation Shorthand
/* animation: name duration timing-function delay iteration-count direction fill-mode play-state */
.element {
animation: slideIn 1s ease-out 0.5s 1 normal forwards running;
}
/* Practical example */
.notification {
animation: slideDown 0.3s ease-out forwards;
}
/* Multiple animations */
.fancy {
animation:
fadeIn 0.5s ease-out,
slideUp 0.5s ease-out,
pulse 2s ease-in-out 0.5s infinite;
}
6. Timing Functions Explained
Timing functions (also called easing functions) control the rate of change during an animation. They're crucial for creating natural, polished motion.
Built-in Keywords
/* Linear - constant speed, mechanical feel */
.linear {
animation-timing-function: linear;
}
/* Ease - default, gentle acceleration and deceleration */
.ease {
animation-timing-function: ease;
}
/* Ease-in - slow start, good for exits */
.ease-in {
animation-timing-function: ease-in;
}
/* Ease-out - fast start, good for entrances */
.ease-out {
animation-timing-function: ease-out;
}
/* Ease-in-out - smooth start and end */
.ease-in-out {
animation-timing-function: ease-in-out;
}
cubic-bezier() - Custom Curves
For precise control, use cubic-bezier() with four values defining the curve:
/* cubic-bezier(x1, y1, x2, y2) */
/* Material Design standard easing */
.material-standard {
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Material Design deceleration */
.material-decelerate {
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
/* Material Design acceleration */
.material-accelerate {
animation-timing-function: cubic-bezier(0.4, 0, 1, 1);
}
/* Custom bounce effect */
.bounce-in {
animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
You can design custom cubic-bezier curves using tools like our CSS Animation Generator or browser DevTools.
steps() - Frame-by-Frame Animation
The steps() function creates discrete, frame-by-frame animations instead of smooth transitions:
/* steps(number-of-steps, direction) */
/* Sprite animation - 8 frames */
@keyframes spriteAnimation {
to { background-position: -800px 0; }
}
.sprite {
width: 100px;
height: 100px;
background: url('sprite-sheet.png') no-repeat;
animation: spriteAnimation 1s steps(8) infinite;
}
/* Typing effect */
@keyframes typing {
from { width: 0; }
to { width: 100%; }
}
.typewriter {
overflow: hidden;
white-space: nowrap;
animation: typing 3s steps(30) forwards;
}
/* step-start: jump to end immediately */
.step-start {
animation-timing-function: step-start;
}
/* step-end: hold start value until end */
.step-end {
animation-timing-function: step-end;
}
Timing Function Best Practices
- Entrances: Use
ease-out(fast start, slow end) - elements arrive quickly and settle gently - Exits: Use
ease-in(slow start, fast end) - elements start slowly then quickly disappear - Movement within view: Use
ease-in-outfor smooth, natural motion - Mechanical/constant motion: Use
linear(loading spinners, scrolling marquees) - Playful, bouncy effects: Use custom
cubic-bezier()with values outside 0-1 range
7. Transform Animations
The transform property is your best friend for performant animations. Unlike animating layout properties (width, height, top, left), transforms are handled by the GPU and don't trigger expensive layout recalculations.
translate() - Position Changes
/* Move element 100px right, 50px down */
.translate {
transform: translate(100px, 50px);
}
/* Individual axis transforms */
.translate-x {
transform: translateX(100px);
}
.translate-y {
transform: translateY(50px);
}
/* Percentage-based (relative to element size) */
.center {
transform: translate(-50%, -50%);
}
/* Slide-in animation */
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
rotate() - Rotation
/* Rotate 45 degrees clockwise */
.rotate {
transform: rotate(45deg);
}
/* Negative values rotate counter-clockwise */
.rotate-ccw {
transform: rotate(-45deg);
}
/* Full rotation animation */
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: spin 1s linear infinite;
}
scale() - Sizing
/* Scale to 150% */
.scale-up {
transform: scale(1.5);
}
/* Scale to 50% */
.scale-down {
transform: scale(0.5);
}
/* Different X and Y scaling */
.scale-stretch {
transform: scale(1.5, 0.8);
}
/* Individual axis */
.scale-x {
transform: scaleX(1.5);
}
/* Pulse animation */
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
.pulsing-button {
animation: pulse 2s ease-in-out infinite;
}
skew() - Distortion
/* Skew along X axis */
.skew-x {
transform: skewX(20deg);
}
/* Skew along Y axis */
.skew-y {
transform: skewY(10deg);
}
/* Both axes */
.skew-both {
transform: skew(20deg, 10deg);
}
Combining Transforms
You can chain multiple transform functions in a single declaration:
/* Order matters! These produce different results */
.combined-1 {
transform: rotate(45deg) translateX(100px);
}
.combined-2 {
transform: translateX(100px) rotate(45deg);
}
/* Complex animation */
@keyframes complexTransform {
0% {
transform: translateX(0) rotate(0deg) scale(1);
}
50% {
transform: translateX(200px) rotate(180deg) scale(1.5);
}
100% {
transform: translateX(0) rotate(360deg) scale(1);
}
}
3D Transforms
CSS also supports 3D transformations for depth and perspective:
/* Set up 3D context */
.container-3d {
perspective: 1000px;
}
/* 3D rotations */
.rotate-3d {
transform: rotateX(45deg) rotateY(45deg);
transform-style: preserve-3d;
}
/* Z-axis translation (towards/away from viewer) */
.translate-z {
transform: translateZ(100px);
}
/* Card flip animation */
@keyframes flip {
from {
transform: rotateY(0deg);
}
to {
transform: rotateY(180deg);
}
}
.card {
transform-style: preserve-3d;
transition: transform 0.6s;
}
.card:hover {
transform: rotateY(180deg);
}
For more complex transforms and visual previews, check out our CSS Gradient Generator and Box Shadow Generator for related visual effects.
8. Common Animation Patterns
Here are battle-tested animation patterns you can adapt for your projects.
Fade Effects
/* Fade in */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Fade out */
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
/* Fade in up */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
Slide Effects
/* Slide in from right */
@keyframes slideInRight {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
/* Slide in from left */
@keyframes slideInLeft {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
/* Slide down (for dropdown menus) */
@keyframes slideDown {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
Bounce Effect
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
}
.bounce-element {
animation: bounce 1s ease-in-out;
}
Shake Effect
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
10%, 30%, 50%, 70%, 90% {
transform: translateX(-10px);
}
20%, 40%, 60%, 80% {
transform: translateX(10px);
}
}
/* Use for error states */
.error-input {
animation: shake 0.5s;
}
Loading Spinner
/* Classic circular spinner */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* Dots spinner */
@keyframes dotPulse {
0%, 100% {
transform: scale(0.6);
opacity: 0.5;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.dot {
width: 12px;
height: 12px;
background: #3498db;
border-radius: 50%;
display: inline-block;
animation: dotPulse 1.4s ease-in-out infinite;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
Progress Bar
/* Indeterminate progress bar */
@keyframes progressIndeterminate {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(400%);
}
}
.progress-bar {
width: 100%;
height: 4px;
background: #e0e0e0;
overflow: hidden;
position: relative;
}
.progress-bar-fill {
width: 25%;
height: 100%;
background: #3498db;
animation: progressIndeterminate 1.5s ease-in-out infinite;
}
/* Shimmer loading effect */
@keyframes shimmer {
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
}
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 0%,
#e0e0e0 50%,
#f0f0f0 100%
);
background-size: 1000px 100%;
animation: shimmer 2s infinite;
}
9. Performance Best Practices
Not all CSS properties are created equal when it comes to animation performance. Understanding how browsers render animations is crucial for smooth, 60fps experiences.
The Rendering Pipeline
When you animate a CSS property, the browser goes through up to three steps:
- Layout (Reflow): Calculating element positions and sizes
- Paint: Filling in pixels (colors, shadows, borders)
- Composite: Drawing layers in the correct order
Different properties trigger different steps:
Property Performance Impact
- SLOW (Layout + Paint + Composite): width, height, padding, margin, top, left, border, font-size
- MEDIUM (Paint + Composite): color, background-color, box-shadow, border-radius
- FAST (Composite only): transform, opacity
Compositor-Only Properties
Stick to transform and opacity for smooth 60fps animations:
/* ❌ BAD - triggers layout */
.slow-animation {
transition: left 0.3s, width 0.3s;
}
.slow-animation:hover {
left: 100px;
width: 200px;
}
/* ✅ GOOD - compositor only */
.fast-animation {
transition: transform 0.3s, opacity 0.3s;
}
.fast-animation:hover {
transform: translateX(100px) scaleX(1.5);
opacity: 0.8;
}
will-change Property
The will-change property hints to the browser that an element will be animated, allowing it to optimize ahead of time:
/* Tell browser to prepare for transform animations */
.animated-element {
will-change: transform;
}
/* Multiple properties */
.complex-animation {
will-change: transform, opacity;
}
will-change Warnings
- Don't overuse: Only apply to elements you're actively animating. Too many will-change declarations can hurt performance.
- Remove when done: Set
will-change: autoafter animations complete to free up resources. - Don't use on all elements: Use sparingly on specific animated elements, not blanket
*selectors.
Hardware Acceleration Trick
Force GPU acceleration by adding a 3D transform:
/* Force hardware acceleration */
.gpu-accelerated {
transform: translateZ(0);
/* or */
transform: translate3d(0, 0, 0);
}
/* Modern approach: use will-change instead */
.modern-gpu-accelerated {
will-change: transform;
}
Avoiding Layout Thrashing
Reading layout properties (like offsetHeight) then immediately writing them causes forced synchronous layout (layout thrashing):
/* ❌ BAD - causes layout thrashing in JS */
elements.forEach(el => {
const height = el.offsetHeight; // read
el.style.height = height + 10 + 'px'; // write
});
/* ✅ GOOD - batch reads, then writes */
const heights = elements.map(el => el.offsetHeight); // batch reads
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // batch writes
});
/* ✅ BEST - use CSS transforms instead */
elements.forEach(el => {
el.style.transform = 'scaleY(1.1)';
});
Performance Checklist
- Animate
transformandopacitywhen possible - Use
will-changesparingly on elements you'll animate - Avoid animating width, height, top, left, margin, padding
- Keep animation durations under 1 second for most UI interactions
- Use DevTools Performance panel to identify janky animations
- Test on lower-end devices and throttle CPU in DevTools
- Reduce motion complexity on mobile devices
- Consider using
contain: layoutto isolate expensive elements
For production-ready, performance-optimized animations, use our CSS Animation Generator which follows these best practices automatically.
10. Animation with JavaScript
While CSS animations handle most use cases, JavaScript gives you dynamic control and complex sequencing capabilities.
Web Animations API
The Web Animations API brings CSS animation power to JavaScript with precise control:
// Basic animation
const element = document.querySelector('.box');
element.animate(
[
{ transform: 'translateX(0px)' },
{ transform: 'translateX(100px)' }
],
{
duration: 500,
easing: 'ease-out',
fill: 'forwards'
}
);
// With control
const animation = element.animate(
[
{ opacity: 0, transform: 'scale(0.5)' },
{ opacity: 1, transform: 'scale(1)' }
],
{
duration: 300,
easing: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)'
}
);
// Control playback
animation.pause();
animation.play();
animation.reverse();
animation.cancel();
// Listen for completion
animation.finished.then(() => {
console.log('Animation complete!');
});
requestAnimationFrame
For custom animations or canvas work, use requestAnimationFrame for smooth 60fps updates:
// Smooth scrolling animation
function smoothScroll(targetY, duration) {
const startY = window.pageYOffset;
const distance = targetY - startY;
const startTime = performance.now();
function animate(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Ease-out cubic
const easing = 1 - Math.pow(1 - progress, 3);
window.scrollTo(0, startY + distance * easing);
if (progress < 1) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
// Usage
smoothScroll(1000, 600); // Scroll to 1000px over 600ms
Intersection Observer for Scroll Animations
Trigger animations when elements enter the viewport:
// HTML
<div class="fade-in-on-scroll">Content</div>
// CSS
.fade-in-on-scroll {
opacity: 0;
transform: translateY(50px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.fade-in-on-scroll.visible {
opacity: 1;
transform: translateY(0);
}
// JavaScript
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
},
{
threshold: 0.1, // Trigger when 10% visible
rootMargin: '0px 0px -100px 0px' // Trigger 100px before viewport
}
);
// Observe all elements
document.querySelectorAll('.fade-in-on-scroll').forEach(el => {
observer.observe(el);
});
Class Toggle Pattern
The simplest and most maintainable approach—let CSS handle the animation, use JS to toggle classes:
// CSS defines the animation
.modal {
opacity: 0;
transform: scale(0.9) translateY(-50px);
transition: opacity 0.3s ease-out, transform 0.3s ease-out;
pointer-events: none;
}
.modal.active {
opacity: 1;
transform: scale(1) translateY(0);
pointer-events: auto;
}
// JavaScript just toggles the class
const modal = document.querySelector('.modal');
function openModal() {
modal.classList.add('active');
}
function closeModal() {
modal.classList.remove('active');
}
11. Accessibility Considerations
Animations can cause real harm to users with vestibular disorders, seizure disorders, or attention difficulties. Responsible animation design is not optional—it's a fundamental accessibility requirement.
prefers-reduced-motion
The prefers-reduced-motion media query detects users who have requested reduced motion in their OS settings:
/* Default: full animations */
.animated-element {
animation: slideIn 0.5s ease-out;
}
/* Respect user preference */
@media (prefers-reduced-motion: reduce) {
.animated-element {
animation: none;
}
}
/* Better: show instant state changes */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Mobile-First Reduced Motion
Consider making reduced motion the default on mobile, with full animations opt-in:
/* Mobile: minimal animations by default */
.card {
transition: opacity 0.2s ease;
}
/* Desktop: full animations if not reduced-motion */
@media (min-width: 768px) and (prefers-reduced-motion: no-preference) {
.card {
transition: transform 0.3s ease-out, opacity 0.3s ease-out, box-shadow 0.3s ease-out;
}
.card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0,0,0,0.2);
}
}
Safe Animation Guidelines
- No flashing: Never flash or strobe faster than 3 times per second (seizure risk)
- Parallax caution: Parallax scrolling can trigger vestibular issues—use sparingly and respect reduced-motion
- Auto-play limits: Don't auto-play animations for more than 5 seconds unless user can pause
- Infinite loops: Provide controls to pause infinite animations
- Essential motion only: Don't hide critical content behind animation that can't be disabled
Pause/Play Controls
For persistent animations, provide user controls:
<div class="carousel">
<button class="pause-btn" aria-label="Pause animation">
Pause
</button>
</div>
// JavaScript
const carousel = document.querySelector('.carousel');
const pauseBtn = document.querySelector('.pause-btn');
pauseBtn.addEventListener('click', () => {
if (carousel.style.animationPlayState === 'paused') {
carousel.style.animationPlayState = 'running';
pauseBtn.textContent = 'Pause';
} else {
carousel.style.animationPlayState = 'paused';
pauseBtn.textContent = 'Play';
}
});
WCAG 2.1 Animation Requirements
- 2.2.2 Pause, Stop, Hide (Level A): Auto-updating content that lasts more than 5 seconds must have pause/stop controls
- 2.3.1 Three Flashes or Below (Level A): No content flashes more than 3 times per second
- 2.3.3 Animation from Interactions (Level AAA): Motion triggered by interaction can be disabled
Accessibility Testing
Test your animations with:
- OS reduced-motion settings enabled (macOS: System Preferences > Accessibility > Display, Windows: Settings > Ease of Access > Display)
- Screen reader enabled (VoiceOver, NVDA, JAWS)
- Keyboard-only navigation
- CPU throttling in DevTools (simulates lower-end devices)
12. Modern CSS Animation Features
CSS animation capabilities have expanded dramatically in recent years. Here are cutting-edge features available in 2026.
Individual Transform Properties
Instead of combining transforms in a single property, you can now animate them individually:
/* Old way - overwrites entire transform */
.old {
transform: translateX(100px);
transition: transform 0.3s;
}
.old:hover {
transform: scale(1.2); /* translateX is lost! */
}
/* New way - independent properties */
.new {
translate: 100px 0;
scale: 1;
rotate: 0deg;
transition: scale 0.3s;
}
.new:hover {
scale: 1.2; /* translate remains intact */
}
/* Animate each independently */
@keyframes complexMotion {
0% {
translate: 0 0;
rotate: 0deg;
scale: 1;
}
50% {
translate: 100px 0;
rotate: 180deg;
}
100% {
scale: 1.5;
}
}
View Transitions API
The View Transitions API enables smooth, native-like page transitions:
/* CSS */
@keyframes fade-in {
from { opacity: 0; }
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes slide-from-right {
from { transform: translateX(100%); }
}
::view-transition-old(root) {
animation: 0.3s ease-out both fade-out;
}
::view-transition-new(root) {
animation: 0.3s ease-out both fade-in, 0.3s ease-out both slide-from-right;
}
// JavaScript
function updateView() {
if (!document.startViewTransition) {
// Fallback for browsers without support
updateDOM();
return;
}
document.startViewTransition(() => updateDOM());
}
function updateDOM() {
document.body.innerHTML = '<h1>New Content</h1>';
}
Scroll-Linked Animations
Animate elements based on scroll position (experimental, check browser support):
/* Progress bar that fills as you scroll */
@keyframes grow-progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
.progress-bar {
animation: grow-progress auto linear;
animation-timeline: scroll();
transform-origin: left;
}
/* Fade in elements as they enter viewport */
@keyframes fade-in-view {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.scroll-reveal {
animation: fade-in-view linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
Container Query Animations
Animate based on container size, not viewport:
.container {
container-type: inline-size;
}
/* Change animation based on container width */
@container (min-width: 600px) {
.card {
animation: slideInRight 0.5s ease-out;
}
}
@container (max-width: 599px) {
.card {
animation: fadeIn 0.3s ease-out;
}
}
CSS @supports for Progressive Enhancement
Use feature detection to provide fallbacks:
/* Default animation */
.element {
animation: fadeIn 0.5s ease-out;
}
/* Enhanced animation if view-timeline is supported */
@supports (animation-timeline: view()) {
.element {
animation: fadeInUp linear both;
animation-timeline: view();
}
}
/* Fallback to JS if container queries not supported */
@supports not (container-type: inline-size) {
.fallback-element {
animation: basicFade 0.3s ease;
}
}
Browser Support in 2026
Most modern animation features have excellent support in 2026:
- Wide support: Individual transform properties, view transitions (major browsers)
- Growing support: Scroll-linked animations (check caniuse.com)
- Always progressive enhance: Use @supports and provide fallbacks
Performance Monitoring
Use the Performance API to monitor animation performance:
// Detect dropped frames
let lastTime = performance.now();
let frames = 0;
function checkFrameRate() {
const now = performance.now();
frames++;
if (now >= lastTime + 1000) {
const fps = Math.round((frames * 1000) / (now - lastTime));
console.log(`FPS: ${fps}`);
frames = 0;
lastTime = now;
}
requestAnimationFrame(checkFrameRate);
}
checkFrameRate();
Keep Learning
CSS animations are evolving rapidly. Stay up to date with:
- The CSS Animations spec on W3C
- Browser release notes (Chrome, Firefox, Safari)
- DevToolbox's blog for practical tutorials and tips
Experiment with our CSS Animation Generator, CSS Beautifier, and CSS Minifier to streamline your animation workflow.
Conclusion
CSS animations are a powerful tool for creating engaging, performant web experiences. By understanding the fundamentals—transitions vs. animations, keyframes, timing functions, and transforms—you can create smooth, professional motion design.
Remember the key principles:
- Performance first: Stick to transform and opacity
- Accessibility always: Respect prefers-reduced-motion
- Progressive enhancement: Use @supports for modern features
- Purpose-driven: Every animation should serve a UX goal
Whether you're building subtle hover effects or complex loading sequences, CSS animations give you the tools to bring your interfaces to life—without sacrificing performance or accessibility.
For more CSS resources, explore our CSS Flexbox Cheat Sheet and CSS Grid Cheat Sheet to master layout alongside animation.