Back to All Articles
cssjavascriptanimationperformanceui

Building High-Performance Web Animations with CSS and JavaScript

Learn how to build silky smooth UI animations combining CSS transitions and JavaScript hooks without sacrificing mobile frame rates.

2026-10-13
Building High-Performance Web Animations with CSS and JavaScript

Let's look at how to build hardware-accelerated UI animations that run at a crisp 60fps on both desktop and mobile devices.

What you'll learn:

  • How to trigger layout-safe CSS transitions using React state hooks.
  • Offloading heavy calculations to requestAnimationFrame for buttery smooth scrolling.
  • Creating fluid interactive hover states with proper easing functions.
  • Respecting user accessibility preferences for reduced motion.

Structuring Smooth CSS Transitions in React

When building interactive elements like dropdowns, modals, or sliding panels, triggering layout changes via top/left properties forces the browser to recalculate layout constantly. Instead, we want to animate using transform and opacity properties. These run on the compositor thread, keeping the main thread free for JavaScript execution.

Here is a clean component that toggles a lateral slider panel using Tailwind CSS classes for transforms.

TSX
import { useState } from 'react';

export function LateralSlider() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="relative overflow-hidden">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="px-4 py-2 bg-indigo-600 text-white rounded-md font-medium"
      >
        {isOpen ? 'Close Panel' : 'Open Panel'}
      </button>

      {/* Backdrop */}
      <div
        className={`fixed inset-0 bg-black/50 transition-opacity duration-300 ${
          isOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'
        }`}
        onClick={() => setIsOpen(false)}
      />

      {/* Sliding Panel */}
      <div
        className={`fixed top-0 right-0 h-full w-80 bg-white shadow-2xl p-6 transform transition-transform duration-300 ease-out ${
          isOpen ? 'translate-x-0' : 'translate-x-full'
        }`}
      >
        <h2 className="text-xl font-bold mb-4">Settings Menu</h2>
        <p className="text-gray-600">Configure your application parameters here.</p>
      </div>
    </div>
  );
}

Speed Note: Always use will-change: transform sparingly or let the browser handle it via hardware-accelerated classes like translate-x-*. Overusing will-change exhausts GPU memory on mobile devices.

Building Scroll-Triggered Interactions with Intersection Observer

Scroll animations add polish, but attaching heavy event listeners directly to the scroll event will tank your frame rate. Instead, we use the IntersectionObserver API to monitor elements entering the viewport asynchronously.

Here is a simple React hook wrapper that triggers state changes when an element scrolls into view.

TSX
import { useEffect, useRef, useState } from 'react';

export function useScrollReveal(threshold = 0.1) {
  const ref = useRef<HTMLDivElement>(null);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.unobserve(element); // Trigger only once
        }
      },
      { threshold }
    );

    observer.observe(element);
    return () => observer.disconnect();
  }, [threshold]);

  return { ref, isVisible };
}

Gotcha: Always unobserve elements once the animation fires if you don't need re-triggering. Leaving observers active on long pages consumes unnecessary memory.

Assembling Scroll-Animated Cards

Now let's apply our scroll hook to build a card component that fades and slides up smoothly as the user scrolls down the page.

TSX
import { useScrollReveal } from './useScrollReveal';

export function ScrollCard({ title, description }: { title: string; description: string }) {
  const { ref, isVisible } = useScrollReveal(0.2);

  return (
    <div
      ref={ref}
      className={`p-6 bg-slate-900 text-white rounded-xl border border-slate-800 transition-all duration-700 ease-out ${
        isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-12'
      }`}
    >
      <h3 className="text-lg font-semibold mb-2">{title}</h3>
      <p className="text-slate-400 text-sm">{description}</p>
    </div>
  );
}

Accessibility Note: Users prone to motion sickness often enable "Reduce Motion" in their operating system settings. Always check this preference before triggering sweeping transform animations.

Handling Reduced Motion Queries

To respect user accessibility settings, we can read the prefers-reduced-motion media query in CSS or via a custom React hook to disable or tone down heavy transitions.

TSX
import { useEffect, useState } from 'react';

export function usePrefersReducedMotion() {
  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;
}

By wrapping animation classes conditionally, you ensure your app is accessible to everyone without stripping away the core layout structure.

Summary

Combining CSS transitions for hardware-accelerated movement with Intersection Observer APIs for scroll triggers gives you the best of both worlds: buttery smooth animations and clean, maintainable code. Keep layout calculations off the main thread and always respect user preferences.

Next Steps:

  • Audit your current app for layout thrashing by checking Chrome DevTools Performance panel.
  • Implement IntersectionObserver for lazy-loading heavy components alongside your scroll animations.
  • Add prefers-reduced-motion checks to all custom modal and slider transitions.