CSS Animations: The Complete Guide for 2026

| 15 min read

Table of Contents

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:

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 Generator

2. 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:

/* 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 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:

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

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:

  1. Layout (Reflow): Calculating element positions and sizes
  2. Paint: Filling in pixels (colors, shadows, borders)
  3. 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: auto after 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

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

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

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:

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.