Back to All Articles
cssanimationperformanceuireact

Building High-Performance Web Animations with CSS and the Web Animations API

Learn how to build smooth, performant UI animations using modern CSS and the Web Animations API without hurting browser rendering frames.

2026-10-14
Building High-Performance Web Animations with CSS and the Web Animations API

Let's look at how to implement buttery-smooth UI animations using modern CSS and the native Web Animations API (WAAPI) while avoiding layout thrashing.

What you'll learn

  • Why animating layout properties ruins frame rates.
  • How to trigger hardware-accelerated transforms in CSS.
  • Controlling timelines dynamically using the Web Animations API.
  • Handling accessibility preferences for motion-sensitive users.

1. Stick to Composite-Only Properties

If you animate properties like width, height, top, or left, the browser has to recalculate layout and paint every single frame. That drops your frame rate instantly.

Instead, stick strictly to transform and opacity. These run entirely on the compositor thread.

TSX
// styles.css
.card {
  /* Bad: triggers layout recalculations */
  /* transition: top 0.3s ease; */

  /* Good: handled by the GPU compositor */
  transform: translateY(0);
  opacity: 1;
  transition: transform 300ms cubic-bezier(0.16, 1, 0.3, 1), 
              opacity 300ms ease;
}

.card:hover {
  transform: translateY(-4px);
}

Speed Note: Always use will-change: transform sparingly if an element stutters on initial hover, but avoid applying it globally as it consumes excessive GPU memory.

2. Declarative Enter Animations with Tailwind

For basic UI elements like dropdowns or modals entering the DOM, declarative CSS classes combined with Tailwind keep your component logic clean and maintainable.

TSX
// components/Modal.tsx
import React from 'react';

interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
}

export function Modal({ isOpen, onClose, children }: ModalProps) {
  if (!isOpen) return null;

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in">
      <div className="w-full max-w-md rounded-xl bg-white p-6 shadow-2xl transition-all animate-scale-in">
        {children}
        <button 
          onClick={onClose}
          className="mt-4 rounded-lg bg-slate-900 px-4 py-2 text-white hover:bg-slate-800"
        >
          Close
        </button>
      </div>
    </div>
  );
}

3. Dynamic Control with the Web Animations API

When you need precise runtime control—like reversing an animation, scrubbing through timelines, or chaining events—CSS falls short. That is where the Web Animations API shines.

TSX
// components/PulseBox.tsx
'use client';

import React, { useRef } from 'react';

export function PulseBox() {
  const boxRef = useRef<HTMLDivElement>(null);
  const animationRef = useRef<Animation | null>(null);

  const handleMouseEnter = () => {
    if (!boxRef.current) return;

    // Create or play the WAAPI timeline
    if (!animationRef.current) {
      animationRef.current = boxRef.current.animate(
        [
          { transform: 'scale(1)', background: '#3b82f6' },
          { transform: 'scale(1.08)', background: '#60a5fa' },
          { transform: 'scale(1)', background: '#3b82f6' }
        ],
        {
          duration: 600,
          iterations: Infinity,
          easing: 'ease-in-out'
        }
      );
    } else {
      animationRef.current.play();
    }
  };

  const handleMouseLeave = () => {
    animationRef.current?.pause();
  };

  return (
    <div
      ref={boxRef}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      className="h-32 w-32 cursor-pointer rounded-2xl bg-blue-500 shadow-lg"
    />
  );
}

Gotcha: Always hold a reference to the returned Animation object if you intend to pause, reverse, or cancel it later. Letting animations accumulate unboundedly creates memory leaks.

4. Respecting prefers-reduced-motion

Heavy animations can cause severe physical discomfort or disorientation for users with vestibular disorders. Always check user preferences before triggering motion.

TSX
// hooks/useReducedMotion.ts
import { useEffect, useState } from 'react';

export function useReducedMotion(): boolean {
  const [reducedMotion, setReducedMotion] = useState(false);

  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
    setReducedMotion(mediaQuery.matches);

    const handler = (event: MediaQueryListEvent) => {
      setReducedMotion(event.matches);
    };

    mediaQuery.addEventListener('change', handler);
    return () => mediaQuery.removeEventListener('change', handler);
  }, []);

  return reducedMotion;
}

5. Integrating Motion States Safely in React

Combine your reduced motion hook directly into your animation triggers so components gracefully fall back to instant state changes when requested.

TSX
// components/SmoothCard.tsx
'use client';

import React from 'react';
import { useReducedMotion } from '@/hooks/useReducedMotion';

export function SmoothCard({ title }: { title: string }) {
  const shouldReduceMotion = useReducedMotion();

  const animationClass = shouldReduceMotion
    ? 'transition-none'
    : 'transition-all duration-300 hover:-translate-y-1 hover:shadow-xl';

  return (
    <div className={`rounded-xl border border-slate-200 bg-white p-6 shadow-sm ${animationClass}`}>
      <h3 className="text-lg font-semibold text-slate-900">{title}</h3>
      <p className="mt-2 text-sm text-slate-600">
        This card respects user accessibility settings automatically.
      </p>
    </div>
  );
}

Summary & Next Steps

High-performance web animation is all about offloading work to the GPU compositor thread, leveraging native browser APIs when complexity grows, and respecting user accessibility settings.

  • Next Steps:
    • Audit your current app for layout-thrashing properties like height transitions.
    • Implement useReducedMotion across all interactive component libraries.
    • Experiment with the Web Animations API for timeline scrubbing on scroll events.