# Aurora Background (Moonarc)

Ambient gradient field: three soft colour fields drifting at three coprime speeds, a slow hue rotation so the colours themselves move, and a film-grain layer stepped at 12 fps. No WebGL, no canvas, no blur filter: every layer is a transform on the compositor.

- Import: `import Aurora from '@moonarc/core/Aurora'`
- Install: `npx astro add moonarc` · copy-paste: `npx shadcn@latest add https://moonarc.dev/r/aurora.json`
- Tier A · category background · trigger always
- Readout: `<Aurora duration={22}>`
- Browser support: widely (every browser)
- Measured cost: 0 B JS (CSS 6.5 kB raw)
- Page: https://moonarc.dev/components/aurora/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `colors` | `[string, string, string]` | `violet, cyan, rose (oklch)` | Three colours, any CSS colour. Keep chroma moderate on light backgrounds. |
| `duration` | `number` | `22` | Seconds per drift cycle of the slowest field; the others run at 0.72× and 0.55× so the pattern never repeats. Under 12 stops being ambient. |
| `hue` | `number` | `40` | Degrees the hues wander either side of the given colours over 2.6 cycles; 0 pins them. |
| `grain` | `number` | `0.5` | Grain strength, 0–1; 0 removes the layer. |
| `opacity` | `number` | `0.55` | Overall strength, 0–1. |

## Usage

```astro
<section class="relative">
  <Aurora />
  <h1 class="relative">Content above the field</h1>
</section>
```

## Reduced motion

The fields stop drifting, the hue stops turning and the grain holds still: a static gradient with texture.

## With ClientRouter

CSS-only; nothing to rebind.

## Craft

- No filter: blur(). A moving blurred layer is re-blurred every frame; a soft radial gradient moving on the compositor is not.
- alternate + ease-in-out so the drift never snaps back to its start; three coprime periods (1 : 0.72 : 0.55) so the composition never repeats.
- Hue moves with one accelerated hue-rotate on the field, not by repainting gradients; the grain is a static SVG tile whose translate steps at 12 fps.
- contain: strict and pointer-events: none, because the layer is decoration and must never affect layout or clicks.
- aria-hidden: there is nothing to announce.

## Replaces

- Aurora (React Bits, OGL)
- AuroraBackground (Aceternity)
- BackgroundGradientAnimation (Aceternity)

## Source

```astro
---
/**
 * Aurora — ambient gradient field, zero JS, no WebGL.
 *
 * Four layers, each on the compositor: three soft colour fields drifting at
 * three coprime periods so the pattern never repeats, a slow hue rotation on
 * the whole field so the colours themselves move, and a grain layer stepped
 * at 12 fps so the surface reads as film, not vector. No filter: blur() — the
 * softness comes from the gradient falloff itself, so every frame costs a
 * transform (and one accelerated hue-rotate), never a re-blur. Fills its
 * positioned parent.
 */
import type { HTMLAttributes } from 'astro/types';

interface Props extends HTMLAttributes<'div'> {
  /** Three colours, any CSS colour. */
  colors?: [string, string, string];
  /** Seconds per drift cycle of the slowest field; the others run at 0.72× and 0.55×. */
  duration?: number;
  /** Overall strength, 0–1. */
  opacity?: number;
  /** Degrees the hues wander either side of the given colours; 0 pins them. */
  hue?: number;
  /** Grain strength, 0–1; 0 removes the layer. */
  grain?: number;
}

const {
  colors = ['oklch(70% 0.19 300)', 'oklch(78% 0.15 200)', 'oklch(75% 0.17 350)'],
  duration = 22,
  opacity = 0.55,
  hue = 40,
  grain = 0.5,
  class: className,
  style,
  ...rest
} = Astro.props;
const vars = [
  `--ma-aurora-1:${colors[0]}`,
  `--ma-aurora-2:${colors[1]}`,
  `--ma-aurora-3:${colors[2]}`,
  `--ma-aurora-duration:${duration}s`,
  `--ma-aurora-opacity:${opacity}`,
  `--ma-aurora-hue:${hue}deg`,
  `--ma-aurora-grain:${grain}`,
  typeof style === 'string' ? style : '',
]
  .filter(Boolean)
  .join(';');
---

<div
  class:list={['ma-aurora', className]}
  aria-hidden="true"
  data-grain={grain > 0 ? '' : undefined}
  style={vars}
  {...rest}
>
  <span class="ma-aurora__field">
    <span class="ma-aurora__blob"></span>
    <span class="ma-aurora__blob"></span>
    <span class="ma-aurora__blob"></span>
  </span>
</div>

<style is:global>
  @layer components {
    :where(.ma-aurora) {
      position: absolute;
      inset: 0;
      overflow: hidden;
      pointer-events: none;
      opacity: var(--ma-aurora-opacity, 0.55);
      contain: strict;
    }
    :where(.ma-aurora__field) {
      position: absolute;
      inset: -10%;
      will-change: filter;
      animation: ma-aurora-hue calc(var(--ma-aurora-duration, 22s) * 2.6) ease-in-out infinite alternate;
    }
    :where(.ma-aurora__blob) {
      position: absolute;
      width: 70%;
      aspect-ratio: 1;
      border-radius: 50%;
      background: radial-gradient(circle at center, var(--ma-aurora-c) 0%, color-mix(in oklch, var(--ma-aurora-c), transparent 55%) 28%, transparent 64%);
      will-change: translate, scale;
      animation: ma-aurora-drift var(--ma-aurora-duration, 22s) ease-in-out infinite alternate;
    }
    :where(.ma-aurora__blob:nth-child(1)) {
      --ma-aurora-c: var(--ma-aurora-1);
      top: -25%;
      left: -15%;
    }
    :where(.ma-aurora__blob:nth-child(2)) {
      --ma-aurora-c: var(--ma-aurora-2);
      top: 5%;
      right: -20%;
      animation-name: ma-aurora-drift-2;
      animation-duration: calc(var(--ma-aurora-duration, 22s) * 0.72);
      animation-delay: calc(var(--ma-aurora-duration, 22s) / -3);
    }
    :where(.ma-aurora__blob:nth-child(3)) {
      --ma-aurora-c: var(--ma-aurora-3);
      bottom: -35%;
      left: 15%;
      animation-name: ma-aurora-drift-3;
      animation-duration: calc(var(--ma-aurora-duration, 22s) * 0.55);
      animation-delay: calc(var(--ma-aurora-duration, 22s) / -1.5);
    }
    /* grain: a tiled SVG turbulence, stepped so it flickers like film at 12 fps; translate only, so it stays on the compositor */
    :where(.ma-aurora[data-grain])::after {
      content: '';
      position: absolute;
      inset: -20%;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' 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 0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
      background-size: 200px 200px;
      mix-blend-mode: overlay;
      opacity: calc(var(--ma-aurora-grain, 0.5) * 0.5);
      animation: ma-aurora-grain 0.5s steps(6) infinite;
    }
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-aurora__field),
      :where(.ma-aurora__blob),
      :where(.ma-aurora[data-grain])::after {
        animation: none;
        will-change: auto;
      }
    }
  }

  @keyframes ma-aurora-drift {
    0% {
      translate: 0 0;
      scale: 1;
    }
    50% {
      translate: 28% 14%;
      scale: 1.25;
    }
    100% {
      translate: -14% 30%;
      scale: 0.9;
    }
  }
  @keyframes ma-aurora-drift-2 {
    0% {
      translate: 0 0;
      scale: 1.1;
    }
    50% {
      translate: -30% 20%;
      scale: 0.85;
    }
    100% {
      translate: -10% -18%;
      scale: 1.2;
    }
  }
  @keyframes ma-aurora-drift-3 {
    0% {
      translate: 0 0;
      scale: 0.95;
    }
    50% {
      translate: 24% -30%;
      scale: 1.2;
    }
    100% {
      translate: -20% -8%;
      scale: 1;
    }
  }
  @keyframes ma-aurora-hue {
    from {
      filter: hue-rotate(calc(var(--ma-aurora-hue, 40deg) * -1));
    }
    to {
      filter: hue-rotate(var(--ma-aurora-hue, 40deg));
    }
  }
  @keyframes ma-aurora-grain {
    to {
      translate: 6% 4%;
    }
  }
</style>

```
