EmpireUI
Get Pro
← Blog8 min read#aurora#background#css animation

Aurora Background Animation in CSS: Three Techniques Compared

Compare three CSS aurora background animation techniques — pure CSS keyframes, canvas, and React hooks — with real code and honest performance tradeoffs.

Green and purple aurora borealis lighting the night sky above mountains

Why Aurora Backgrounds Are Everywhere Right Now

You've seen it. That soft, shifting curtain of green-purple-teal light bleeding across a dark background, making the whole page feel alive without being obnoxious. Aurora backgrounds hit a sweet spot that hard gradients and particle systems miss — organic movement, zero visual noise. They've been exploding in landing pages since roughly 2024, and by 2026 they're practically expected in any dark-mode SaaS.

The problem is there are at least three meaningfully different ways to build one, and they're not interchangeable. Pure CSS keyframe animation, HTML5 canvas with requestAnimationFrame, and a React-driven approach using state + CSS custom properties each have different performance profiles, browser compatibility stories, and levels of control. Pick wrong and you're either burning GPU cycles on a logo page or writing 200 lines of canvas math for something that four CSS keyframes would've solved.

In practice, the decision comes down to one question: does the aurora need to respond to anything (scroll, mouse, data), or is it purely decorative? Decorative? Stop at CSS. Interactive? You'll want the canvas or React approach. That said, let's walk through all three so you know exactly what you're getting into.

One more thing — if you just want a pre-built, accessible component without wrestling with any of this, Empire UI's aurora components handle all three approaches under a single API. But understanding the mechanics yourself is worth the 10 minutes.

Technique 1: Pure CSS Keyframes

This is the lightest option and it's underrated. The trick is stacking 3-4 radial gradients as background-image layers, then animating each one independently using @keyframes that shift background-position and background-size. The gradients bleed into each other and — with the right easing — the result reads as flowing light.

.aurora {
  position: fixed;
  inset: 0;
  background:
    radial-gradient(ellipse 80% 60% at 20% 40%, rgba(120, 80, 255, 0.5), transparent),
    radial-gradient(ellipse 60% 80% at 80% 60%, rgba(0, 200, 180, 0.4), transparent),
    radial-gradient(ellipse 100% 50% at 50% 10%, rgba(60, 130, 255, 0.35), transparent);
  background-size: 200% 200%, 180% 180%, 220% 220%;
  animation: aurora-shift 12s ease-in-out infinite alternate;
}

@keyframes aurora-shift {
  0%   { background-position: 0% 0%,   100% 100%, 50% 0%; }
  33%  { background-position: 50% 100%, 0%   50%,  100% 50%; }
  66%  { background-position: 100% 50%, 50%  0%,   0% 100%; }
  100% { background-position: 30% 80%,  80%  20%,  70% 30%; }
}

The browser runs this entirely on the compositor thread — no JavaScript, no layout recalculation, just GPU work. On a modern MacBook it's effectively zero CPU cost. On a budget Android phone from 2022 you'll still get 60 fps because background-position is a cheap composite property. That's a genuinely big deal when your aurora is a page background that runs for the entire session.

Worth noting: CSS keyframes give you no runtime control. You can't slow the animation on hover without a animation-play-state hack, and you can't tie the colors to any application state. If you need that, read on. But for static landing pages? Honestly, this is all you need.

Quick aside: add @media (prefers-reduced-motion: reduce) { .aurora { animation: none; } } or you'll get complaints from accessibility reviewers on your first PR review.

Technique 2: Canvas + requestAnimationFrame

Canvas gives you pixel-level control. You're drawing Gaussian-blurred blobs programmatically each frame, which means you can vary speed, color, and position in response to any runtime input — mouse position, audio frequency, scroll depth, whatever.

// aurora-canvas.ts
export function initAurora(canvas: HTMLCanvasElement) {
  const ctx = canvas.getContext('2d')!;
  const blobs = [
    { x: 0.2, y: 0.4, r: 0.5, color: 'rgba(120,80,255,0.45)', dx: 0.0003, dy: 0.0002 },
    { x: 0.8, y: 0.6, r: 0.6, color: 'rgba(0,200,180,0.35)', dx: -0.0002, dy: 0.0003 },
    { x: 0.5, y: 0.1, r: 0.7, color: 'rgba(60,130,255,0.3)',  dx: 0.0001, dy: -0.0002 },
  ];

  let raf: number;
  function draw(ts: number) {
    const w = canvas.width;
    const h = canvas.height;
    ctx.clearRect(0, 0, w, h);

    for (const b of blobs) {
      b.x += b.dx * Math.sin(ts * 0.001);
      b.y += b.dy * Math.cos(ts * 0.0007);
      // wrap
      b.x = ((b.x % 1) + 1) % 1;
      b.y = ((b.y % 1) + 1) % 1;

      const grd = ctx.createRadialGradient(
        b.x * w, b.y * h, 0,
        b.x * w, b.y * h, b.r * Math.max(w, h)
      );
      grd.addColorStop(0, b.color);
      grd.addColorStop(1, 'transparent');
      ctx.fillStyle = grd;
      ctx.fillRect(0, 0, w, h);
    }
    raf = requestAnimationFrame(draw);
  }

  raf = requestAnimationFrame(draw);
  return () => cancelAnimationFrame(raf);
}

The tradeoff is real CPU work every frame — this runs on the main thread unless you move it to an OffscreenCanvas worker. For a full-page background that's fine on desktop, but on low-end devices you might clock 8-12 ms per frame just on the canvas draw, which leaves less budget for your UI interactions. A filter: blur(40px) on the canvas element itself (instead of individual blurs per blob) is a common optimization that cuts draw time roughly in half.

Canvas also means you need to handle resize events manually — canvas.width = window.innerWidth on every resize, or the background will stretch and look terrible. Set a debounced resize observer on mount and you're covered. This is one of those things that bites people 3 months after shipping when someone opens the page after resizing their browser.

Look, canvas is the right call when you want the aurora to react. Mouse-following aurora, one that pulses to music, one where the colors shift based on a user's selected theme — all canvas territory. For a pure background that just looks nice? You're paying a runtime cost you don't need to.

Technique 3: React + CSS Custom Properties

This is the sweet spot for React apps. The idea: CSS does the actual rendering (cheap, compositor-threaded), but React drives the custom property values — --aurora-x1, --aurora-y1, --aurora-hue-shift, etc. — via useEffect and requestAnimationFrame. You get runtime control without Canvas overhead, and the CSS transitions between states smoothly.

import { useEffect, useRef } from 'react';

export function AuroraBackground({ className = '' }: { className?: string }) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let raf: number;
    let t = 0;

    function tick() {
      t += 0.004;
      el!.style.setProperty('--a-x1', `${50 + 30 * Math.sin(t)}%`);
      el!.style.setProperty('--a-y1', `${40 + 20 * Math.cos(t * 0.7)}%`);
      el!.style.setProperty('--a-x2', `${50 + 30 * Math.sin(t + 2)}%`);
      el!.style.setProperty('--a-y2', `${60 + 20 * Math.cos(t * 0.8 + 1)}%`);
      el!.style.setProperty('--a-hue', `${(t * 20) % 360}deg`);
      raf = requestAnimationFrame(tick);
    }

    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);

  return (
    <div
      ref={ref}
      className={`aurora-bg ${className}`}
      style={{
        ['--a-x1' as string]: '50%',
        ['--a-y1' as string]: '40%',
      }}
    />
  );
}
.aurora-bg {
  position: fixed;
  inset: 0;
  background:
    radial-gradient(ellipse 70% 55% at var(--a-x1) var(--a-y1),
      hsl(calc(260deg + var(--a-hue, 0deg)) 80% 60% / 0.5), transparent),
    radial-gradient(ellipse 65% 70% at var(--a-x2) var(--a-y2),
      hsl(calc(180deg + var(--a-hue, 0deg)) 70% 55% / 0.4), transparent);
}

This pattern keeps your component tree clean — no canvas element in the DOM, no weird z-index stacking, just a div and CSS. The useEffect cleanup function cancels the animation loop properly when the component unmounts, which is easy to forget with canvas approaches. The hue rotation trick (cycling hsl values via a CSS custom property) is what gives you that color-shifting feel without swapping entire gradient strings.

The one gotcha: style.setProperty on every animation frame does cause style recalculation. It's fast (custom properties don't trigger layout), but it's not free. For 1-3 properties per frame you won't notice. For 10+, consider batching or throttling to 30 fps — if (frame % 2 === 0) update() inside your tick function.

For a polished, ready-to-drop-in version of this, Empire UI's aurora section exports <AuroraBackground> and <AuroraText> components that implement this exact pattern with full TypeScript types, dark mode support, and prefers-reduced-motion handling baked in. Saves about 45 minutes of wiring.

Performance Comparison: When Each Technique Wins

Let's be direct about numbers. Pure CSS keyframes on a full-viewport aurora: ~0ms CPU, compositor-thread only, 60 fps even on a 2021 mid-range phone. Canvas with 3 radial gradients and a filter: blur(48px) on the element: roughly 4-8ms per frame on a MacBook M1, 14-22ms on a Snapdragon 680 device — that's your whole frame budget on a 60 Hz screen.

The React custom-property approach sits in the middle: ~1-2ms per frame for the JS tick, zero extra GPU work beyond what the CSS was already doing. It's the most balanced option for apps where you need some control but can't justify full canvas.

One more thing — will-change: transform on your aurora container doesn't actually help here and can hurt by creating unnecessary GPU layers. I've seen this cargo-culted into aurora implementations constantly. The property is meaningful when you're about to transform an element; it's not a magic performance flag for background-image animations.

For anything deployed in 2026 to users on a broad device range, my default recommendation is: start CSS-only, add the React custom-property layer if you need interactivity, escalate to canvas only if you genuinely need per-pixel control. Don't let the canvas approach seduce you just because it feels more powerful. Power you don't need is just complexity and CPU time.

Combining Aurora With Glassmorphism Cards

An aurora background and glassmorphism cards are a natural pairing — the shifting colors give backdrop-filter: blur() constantly changing material to work with, so the frosted-glass effect looks alive rather than static. It's one of the most visually satisfying combinations you can pull off with pure web tech.

The key is contrast management. Aurora backgrounds are dark, saturated, and shifting, which means your glass card alpha needs to be higher than you'd use over a static gradient — something like rgba(255,255,255,0.12) to 0.18 instead of the typical 0.08. You can also try the glassmorphism generator to dial in values interactively before committing them to CSS variables.

function AuroraPage() {
  return (
    <div className="relative min-h-screen">
      {/* Aurora sits behind everything */}
      <AuroraBackground className="-z-10" />

      {/* Glass cards float over it */}
      <main className="relative z-10 flex items-center justify-center min-h-screen p-8">
        <div className="
          bg-white/15 backdrop-blur-md
          border border-white/20 rounded-3xl
          p-8 max-w-lg shadow-2xl shadow-black/30
        ">
          <h1 className="text-3xl font-bold text-white">Your content here</h1>
        </div>
      </main>
    </div>
  );
}

That shadow-black/30 is doing a lot of work — it grounds the glass card against the aurora and prevents the whole thing from looking like a soup of translucent surfaces. Worth noting: don't stack more than 2-3 glass surfaces at the same depth or the backdrop-filter GPU cost multiplies fast on mobile. Keep the composition simple and the aurora does the visual heavy lifting for you.

FAQ

Is CSS aurora animation bad for performance?

Pure CSS keyframe aurora is compositor-thread only — effectively zero CPU cost and smooth at 60 fps on budget devices. Canvas-based approaches are more expensive; reserve them for interactive or data-driven effects.

Does backdrop-filter work on top of a canvas aurora background?

Yes, and it looks great. Make sure the canvas has a lower z-index than your glass elements and that the canvas doesn't have opacity: 0 (backdrop-filter needs painted pixels behind it).

How do I stop the aurora animation for users who prefer reduced motion?

In CSS, wrap your animation in @media (prefers-reduced-motion: no-preference). In React, read window.matchMedia('(prefers-reduced-motion: reduce)').matches before starting your animation loop and skip it if true.

Can I use aurora backgrounds with Tailwind CSS only?

The keyframe approach works with Tailwind if you define the animation in your tailwind.config.js extend.animation and extend.keyframes blocks. For the CSS custom property approach you'll need a small stylesheet alongside Tailwind.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Aurora UI: How to Build Gradient Aurora Effects in CSS & ReactAurora Card Component in React: Shifting Gradient Glass EffectHow to Create an Aurora Background in React (Free Tailwind)CSS Typewriter Effect: Pure CSS vs JS — When Each Is the Right Call