---
title: Aurora Background
description: Soft colour blobs rising from an edge and drifting behind your content.
category: Backgrounds
tier: free
url: https://ui.dimaac.com/components/aurora-background
registry: https://ui.dimaac.com/r/aurora-background.json
---

# Aurora Background

Four soft colour blobs (teal, mint, violet and pink by default) rise from the bottom, top or middle of a box and drift slowly behind your content, with an optional giant faded wordmark. It is the glow from the DiMaac footer, made into a component. It is built from CSS gradients, one blur per blob and transform-only motion, so it stays light on the GPU, and it pauses while it is off screen. It is tuned separately for light and dark pages (it follows data-theme, a dark class or the system setting, or you can force one), holds still when the visitor prefers reduced motion, and takes your own four colours, a speed, a blur, an intensity and a touch of film grain.

## Install

With the shadcn CLI:

```bash
npx shadcn@latest add https://ui.dimaac.com/r/aurora-background.json
pnpm dlx shadcn@latest add https://ui.dimaac.com/r/aurora-background.json
bunx --bun shadcn@latest add https://ui.dimaac.com/r/aurora-background.json
```

With the `@dimaac` namespace in components.json: `npx shadcn@latest add @dimaac/aurora-background`

Dependencies:

```bash
pnpm add gsap @gsap/react
npm install gsap @gsap/react
```

By hand:

1. Install the packages it uses: gsap, @gsap/react.
2. Copy the component file below into components/dimaac/aurora-background.tsx.
3. It's styled with Tailwind v4 classes. If that folder sits outside the places Tailwind scans, add it with @source in your CSS.
4. Import it with import { AuroraBackground } from "@/components/dimaac/aurora-background" and give its parent a size: it fills it.

## Usage

```tsx
import { AuroraBackground } from "@/components/dimaac/aurora-background";

<AuroraBackground />
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `colors` | `string[]` | - | Four CSS colours for the four blobs, left to right. Overrides `palette`. Fewer than four repeat. |
| `palette` | `AuroraPalette` | `"dimaac"` | A ready-made set of four colours. "dimaac" is teal, mint, violet and pink. |
| `intensity` | `number` | `0.85` | How strong the whole glow is, from 0 (gone) to 1 (full). |
| `blur` | `number` | `48` | How soft the blobs are, as a blur in pixels. More blur melts the colours together. |
| `speed` | `number` | `1` | How fast the blobs drift. 1 is slow and calm, 0 holds them still, 3 is lively. |
| `position` | `"bottom" \| "top" \| "center"` | `"bottom"` | The edge the glow rises from. |
| `wordmark` | `string \| null` | `null` | A giant faded word sitting in the glow. Leave it out (or null) for none. |
| `grain` | `number` | `0` | Film grain over the glow, from 0 (none) to 0.2 (heavy). It hides colour banding. |
| `theme` | `"auto" \| "light" \| "dark"` | `"auto"` | Which tuning to use. "auto" follows the page: data-theme, a dark class, or the system setting. |
| `className` | `string` | `""` | Extra classes for the outer box. Give it a size, or let your content size it. |
| `children` | `ReactNode` | - | Your content. It sits above the glow. |

## Source

### aurora-background.tsx → `components/dimaac/aurora-background.tsx`

```tsx
"use client";

import { useEffect, useRef, useState, useSyncExternalStore, type CSSProperties, type ReactNode } from "react";
import { gsap } from "gsap";
import { useGSAP } from "@gsap/react";

// Soft colour blobs that rise from an edge and drift slowly behind your content, with an optional
// giant faded word in the glow. It is the DiMaac footer aurora, made into a component.
// It stays cheap: every blob is one div with a gradient and a blur, and the drift only moves
// transforms, so the browser's GPU does the work. It pauses when it scrolls out of view.

export type AuroraPalette = "dimaac" | "sunset" | "ocean" | "ember" | "mono";

export type AuroraBackgroundProps = {
  /** Four CSS colours for the four blobs, left to right. Overrides `palette`. Fewer than four repeat. */
  colors?: string[];
  /** A ready-made set of four colours. "dimaac" is teal, mint, violet and pink. */
  palette?: AuroraPalette;
  /** How strong the whole glow is, from 0 (gone) to 1 (full). */
  intensity?: number;
  /** How soft the blobs are, as a blur in pixels. More blur melts the colours together. */
  blur?: number;
  /** How fast the blobs drift. 1 is slow and calm, 0 holds them still, 3 is lively. */
  speed?: number;
  /** The edge the glow rises from. */
  position?: "bottom" | "top" | "center";
  /** A giant faded word sitting in the glow. Leave it out (or null) for none. */
  wordmark?: string | null;
  /** Film grain over the glow, from 0 (none) to 0.2 (heavy). It hides colour banding. */
  grain?: number;
  /** Which tuning to use. "auto" follows the page: data-theme, a dark class, or the system setting. */
  theme?: "auto" | "light" | "dark";
  /** Extra classes for the outer box. Give it a size, or let your content size it. */
  className?: string;
  /** Your content. It sits above the glow. */
  children?: ReactNode;
};

// ---------- colours, tuned once for light and once for dark ----------
// On paper the glow can be strong and the colours deep. On near black the same colours would
// glare, so the mint is turned down and the teal and violet carry the light instead.

const palettes: Record<AuroraPalette, { light: string[]; dark: string[] }> = {
  dimaac: { light: ["#0f766e", "#99f6e4", "#7c3aed", "#f9a8d4"], dark: ["#14b8a6", "#99f6e4", "#7c3aed", "#f9a8d4"] },
  sunset: { light: ["#f97316", "#fde68a", "#e11d48", "#a855f7"], dark: ["#fb923c", "#fde68a", "#e11d48", "#a855f7"] },
  ocean: { light: ["#0284c7", "#a5f3fc", "#4f46e5", "#67e8f9"], dark: ["#38bdf8", "#a5f3fc", "#4f46e5", "#67e8f9"] },
  ember: { light: ["#dc2626", "#fdba74", "#9f1239", "#fb7185"], dark: ["#ef4444", "#fdba74", "#be123c", "#fb7185"] },
  mono: { light: ["#525252", "#d4d4d4", "#262626", "#a3a3a3"], dark: ["#d4d4d4", "#737373", "#a3a3a3", "#525252"] },
};

// How strong each of the four blobs is
const strength = {
  light: [0.55, 0.9, 0.45, 0.55],
  dark: [0.8, 0.38, 0.62, 0.34],
};

const wordmarkInk = {
  light: ["rgba(10,10,10,0.16)", "rgba(10,10,10,0.06)", "rgba(255,255,255,0.35)"],
  dark: ["rgba(255,255,255,0.12)", "rgba(255,255,255,0.04)", "rgba(255,255,255,0.22)"],
};

// Each blob: where it sits across the width (%), how wide and tall it is (% of the glow layer),
// and how it drifts: sideways and up/down in % of its own size, the scale it breathes between,
// the seconds one swing takes, and how far into the swing it starts so they never move together.
const blobs = [
  { x: 18, width: 80, height: 120, driftX: 10, driftY: 9, scale: 1.12, seconds: 9, start: 0 },
  { x: 42, width: 84, height: 140, driftX: -8, driftY: 12, scale: 1.08, seconds: 12, start: 0.35 },
  { x: 66, width: 80, height: 130, driftX: 9, driftY: 8, scale: 1.14, seconds: 10, start: 0.7 },
  { x: 88, width: 60, height: 100, driftX: -11, driftY: 11, scale: 1.1, seconds: 14, start: 0.15 },
] as const;

// Grain: a small tile of grey noise, drawn once as an SVG. Overlay blending keeps it neutral on
// both light and dark pages.
const noise =
  "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 1.6 -0.3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E\")";

// ---------- "is the page dark?" without any site context ----------

function pageIsDark(): boolean {
  const root = document.documentElement;
  const picked = root.getAttribute("data-theme");
  if (picked === "dark") return true;
  if (picked === "light") return false;
  if (root.classList.contains("dark")) return true;
  if (root.classList.contains("light")) return false;
  return window.matchMedia("(prefers-color-scheme: dark)").matches;
}

function watchPageTheme(onChange: () => void): () => void {
  const observer = new MutationObserver(onChange);
  observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "class"] });
  const query = window.matchMedia("(prefers-color-scheme: dark)");
  query.addEventListener("change", onChange);
  return () => {
    observer.disconnect();
    query.removeEventListener("change", onChange);
  };
}

export function AuroraBackground({
  colors,
  palette = "dimaac",
  intensity = 0.85,
  blur = 48,
  speed = 1,
  position = "bottom",
  wordmark = null,
  grain = 0,
  theme = "auto",
  className = "",
  children,
}: AuroraBackgroundProps) {
  const boxRef = useRef<HTMLDivElement>(null);
  const blobRefs = useRef<(HTMLDivElement | null)[]>([]);
  const swings = useRef<gsap.core.Tween[]>([]);
  // Read by the drift so a new speed never rebuilds it
  const speedRef = useRef(speed);
  speedRef.current = speed;

  // On a narrow box (a phone) the blobs would shrink into thin streaks, so they are drawn wider
  const [compact, setCompact] = useState(false);
  useEffect(() => {
    const box = boxRef.current;
    if (!box) return;
    const watch = new ResizeObserver(([entry]) => setCompact((entry?.contentRect.width ?? 1000) < 640));
    watch.observe(box);
    return () => watch.disconnect();
  }, []);

  // The server renders the light tuning; the browser switches to dark right after if it should
  const pageDark = useSyncExternalStore(watchPageTheme, pageIsDark, () => false);
  const mode = theme === "auto" ? (pageDark ? "dark" : "light") : theme;

  // ---------- the drift ----------
  useGSAP(
    () => {
      const box = boxRef.current;
      const elements = blobRefs.current.filter((el): el is HTMLDivElement => el !== null);
      if (!box || elements.length === 0) return;

      const mm = gsap.matchMedia();
      mm.add("(prefers-reduced-motion: no-preference)", () => {
        // One endless back-and-forth swing per blob; yoyo makes it ease out and back with no jump
        swings.current = elements.map((el, i) => {
          const blob = blobs[i]!;
          return gsap.to(el, {
            xPercent: blob.driftX,
            yPercent: -blob.driftY,
            scale: blob.scale,
            duration: blob.seconds,
            ease: "sine.inOut",
            repeat: -1,
            yoyo: true,
            force3D: true,
          });
        });
        swings.current.forEach((swing, i) => {
          swing.progress(blobs[i]!.start);
          swing.timeScale(speedRef.current);
        });

        // Nothing moves while the glow is off screen
        const visibility = new IntersectionObserver(([entry]) => {
          const on = entry?.isIntersecting ?? true;
          swings.current.forEach((swing) => (on ? swing.resume() : swing.pause()));
        });
        visibility.observe(box);

        return () => {
          visibility.disconnect();
          swings.current = [];
        };
      });

      return () => mm.revert();
    },
    { scope: boxRef, dependencies: [position], revertOnUpdate: true },
  );

  // A new speed only changes how fast the swings play
  useEffect(() => {
    swings.current.forEach((swing) => swing.timeScale(Math.max(0, speed)));
  }, [speed]);

  // ---------- what to draw ----------
  const picked = colors && colors.length > 0 ? colors : palettes[palette][mode];
  const colourFor = (i: number) => picked[i % picked.length]!;
  const alphas = strength[mode];
  // Where the blobs are centred, up and down the glow layer
  const centreY = position === "bottom" ? 100 : position === "top" ? 0 : 50;
  // The glow layer runs 30% past the edge it rises from, so the blobs' round bottoms stay off screen
  const layer: CSSProperties =
    position === "bottom" ? { top: 0, height: "130%" } : position === "top" ? { top: "-30%", height: "130%" } : { top: "-15%", height: "130%" };

  const ink = wordmarkInk[mode];
  const wordPlace: CSSProperties =
    position === "bottom"
      ? { bottom: "-0.2em", backgroundImage: `linear-gradient(180deg, ${ink[0]} 0%, ${ink[1]} 55%, ${ink[2]} 100%)` }
      : position === "top"
        ? { top: "-0.2em", backgroundImage: `linear-gradient(0deg, ${ink[0]} 0%, ${ink[1]} 55%, ${ink[2]} 100%)` }
        : { top: "50%", transform: "translateY(-50%)", backgroundImage: `linear-gradient(180deg, ${ink[1]} 0%, ${ink[0]} 50%, ${ink[1]} 100%)` };

  const widen = compact ? 1.7 : 1;
  const safeBlur = Math.max(0, blur);

  return (
    <div ref={boxRef} className={`relative h-full w-full overflow-hidden ${className}`} style={{ containerType: "inline-size" }}>
      <div aria-hidden="true" className="pointer-events-none absolute" style={{ left: "-10%", right: "-10%", opacity: Math.min(1, Math.max(0, intensity)), ...layer }}>
        {blobs.map((blob, i) => (
          <div
            key={i}
            ref={(el) => {
              blobRefs.current[i] = el;
            }}
            className="absolute"
            style={{
              left: `${blob.x - (blob.width * widen) / 2}%`,
              top: `${centreY - blob.height / 2}%`,
              width: `${blob.width * widen}%`,
              height: `${blob.height}%`,
              // Fades from the colour at the heart to nothing at 70% of the way out, like the footer
              backgroundImage: `radial-gradient(closest-side, color-mix(in srgb, ${colourFor(i)} ${alphas[i]! * 100}%, transparent), transparent 70%)`,
              filter: safeBlur > 0 ? `blur(${safeBlur}px)` : undefined,
              willChange: "transform",
            }}
          />
        ))}
      </div>

      {wordmark ? (
        <div
          aria-hidden="true"
          className="pointer-events-none absolute right-0 left-0 text-center leading-none font-medium whitespace-nowrap select-none"
          style={{
            fontSize: "clamp(96px, 25cqw, 380px)",
            letterSpacing: "-0.07em",
            color: "transparent",
            WebkitBackgroundClip: "text",
            backgroundClip: "text",
            ...wordPlace,
          }}
        >
          {wordmark}
        </div>
      ) : null}

      {grain > 0 ? (
        <div
          aria-hidden="true"
          className="pointer-events-none absolute inset-0"
          style={{ backgroundImage: noise, backgroundSize: "160px 160px", mixBlendMode: "overlay", opacity: Math.min(1, grain * 5) }}
        />
      ) : null}

      <div className="relative h-full w-full">{children}</div>
    </div>
  );
}
```

### demo.tsx (the demo on the page, not installed)

```tsx
"use client";

import { AuroraBackground, type AuroraBackgroundProps } from "@/components/dimaac/aurora-background";

// A quiet landing page: paper, ink, and the glow rising from the bottom edge.
// The playground's values arrive as props and go straight to the component.
export default function Demo(props: Partial<AuroraBackgroundProps>) {
  return (
    <div className="h-full w-full bg-[#fafafa] text-[#0a0a0a] dark:bg-[#0b0b0c] dark:text-[#ededed]">
      <AuroraBackground {...props} className={props.className ?? ""}>
        <div className="flex h-full flex-col px-6 py-6 sm:px-10">
          <span className="text-[13px] font-medium tracking-tight">DiMaac</span>

          <div className="flex flex-1 flex-col items-center justify-start pt-[16vh] text-center">
            <h1 className="max-w-[14ch] text-[clamp(44px,8vw,112px)] leading-[0.95] font-medium tracking-[-0.045em]">Light that rises slowly</h1>
            <p className="mt-6 max-w-[38ch] text-[15px] leading-relaxed opacity-60">A calm backdrop made from nothing but gradients. Switch the site theme and watch it retune itself.</p>
            <div className="mt-8 flex gap-3">
              <button type="button" className="rounded-full bg-[#0a0a0a] px-5 py-2.5 text-[13px] font-medium text-[#fafafa] dark:bg-[#ededed] dark:text-[#0b0b0c]">
                Get started
              </button>
              <button type="button" className="rounded-full border border-current/20 px-5 py-2.5 text-[13px] font-medium">
                Browse components
              </button>
            </div>
          </div>
        </div>
      </AuroraBackground>
    </div>
  );
}
```

## Agent prompt

```text
Add the Aurora Background component (<AuroraBackground />) from DiMaac UI to this project.

Docs: https://ui.dimaac.com/components/aurora-background
Source and dependencies (shadcn registry JSON): https://ui.dimaac.com/r/aurora-background.json

Install it with:
npx shadcn@latest add https://ui.dimaac.com/r/aurora-background.json

Then use it like this:
import { AuroraBackground } from "@/components/dimaac/aurora-background";
<AuroraBackground />

If the shadcn CLI isn't set up here, fetch the registry JSON above, write files[0].content to components/dimaac/aurora-background.tsx and install gsap, @gsap/react.
It uses Tailwind v4 classes, so make sure Tailwind scans that folder. Give its parent a size, keep light and dark mode working and keep its prefers-reduced-motion fallback.
Full index for agents: https://ui.dimaac.com/llms.txt
```
