---
title: Aurora
description: WebGL aurora field with tunable color stops, speed, amplitude, and blend.
group: Backgrounds
order: 10
new: true
---

A WebGL aurora field for heroes and quiet backdrops. Tune the color stops, speed, amplitude, and blend.

```tsx
import { Aurora } from "@/components/motion/aurora"

export function Example() {
  return <Aurora className="min-h-72" />
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add aurora
```

## Usage

```tsx
import { Aurora } from "@/components/motion/aurora"
```

```tsx
<Aurora className="min-h-72" />
```

## API Reference

### Aurora

| Prop | Type | Default |
| --- | --- | --- |
| colors | [string, string, string] | ["#3a29ff", "#ff94b4", "#ff3232"] |
| speed | number | 1 |
| amplitude | number | 1 |
| blend | number | 0.5 |
| className | string | — |
| children | ReactNode | — |

Also accepts the other native `div` attributes. Uses `ogl` for the WebGL field.

## Source

```tsx
"use client";

import "@/lib/motion-styles";

import {
  useEffect,
  useRef,
  type ComponentProps,
  type ReactNode,
} from "react";
import { Renderer, Program, Mesh, Color, Triangle } from "ogl";
import { cn } from "@/lib/utils";

export type AuroraProps = Omit<ComponentProps<"div">, "children" | "color"> & {
  colors?: [string, string, string];
  speed?: number;
  amplitude?: number;
  blend?: number;
  children?: ReactNode;
};

const DEFAULT_COLORS = ["#3a29ff", "#ff94b4", "#ff3232"] as const;

const VERT = `#version 300 es
in vec2 position;
void main() {
  gl_Position = vec4(position, 0.0, 1.0);
}
`;

const FRAG = `#version 300 es
precision highp float;

uniform float uTime;
uniform float uAmplitude;
uniform vec3 uColorStops[3];
uniform vec2 uResolution;
uniform float uBlend;

out vec4 fragColor;

vec3 permute(vec3 x) {
  return mod(((x * 34.0) + 1.0) * x, 289.0);
}

float snoise(vec2 v) {
  const vec4 C = vec4(
    0.211324865405187, 0.366025403784439,
    -0.577350269189626, 0.024390243902439
  );
  vec2 i = floor(v + dot(v, C.yy));
  vec2 x0 = v - i + dot(i, C.xx);
  vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
  vec4 x12 = x0.xyxy + C.xxzz;
  x12.xy -= i1;
  i = mod(i, 289.0);

  vec3 p = permute(
    permute(i.y + vec3(0.0, i1.y, 1.0))
    + i.x + vec3(0.0, i1.x, 1.0)
  );

  vec3 m = max(
    0.5 - vec3(
      dot(x0, x0),
      dot(x12.xy, x12.xy),
      dot(x12.zw, x12.zw)
    ),
    0.0
  );
  m = m * m;
  m = m * m;

  vec3 x = 2.0 * fract(p * C.www) - 1.0;
  vec3 h = abs(x) - 0.5;
  vec3 ox = floor(x + 0.5);
  vec3 a0 = x - ox;
  m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);

  vec3 g;
  g.x = a0.x * x0.x + h.x * x0.y;
  g.yz = a0.yz * x12.xz + h.yz * x12.yw;
  return 130.0 * dot(m, g);
}

struct ColorStop {
  vec3 color;
  float position;
};

#define COLOR_RAMP(colors, factor, finalColor) { \
  int index = 0; \
  for (int i = 0; i < 2; i++) { \
    ColorStop currentColor = colors[i]; \
    bool isInBetween = currentColor.position <= factor; \
    index = int(mix(float(index), float(i), float(isInBetween))); \
  } \
  ColorStop currentColor = colors[index]; \
  ColorStop nextColor = colors[index + 1]; \
  float range = nextColor.position - currentColor.position; \
  float lerpFactor = (factor - currentColor.position) / range; \
  finalColor = mix(currentColor.color, nextColor.color, lerpFactor); \
}

void main() {
  vec2 uv = gl_FragCoord.xy / uResolution;

  ColorStop colors[3];
  colors[0] = ColorStop(uColorStops[0], 0.0);
  colors[1] = ColorStop(uColorStops[1], 0.5);
  colors[2] = ColorStop(uColorStops[2], 1.0);

  vec3 rampColor;
  COLOR_RAMP(colors, uv.x, rampColor);

  float height = snoise(vec2(uv.x * 2.0 + uTime * 0.1, uTime * 0.25)) * 0.5 * uAmplitude;
  height = exp(height);
  height = (uv.y * 2.0 - height + 0.2);
  float intensity = 0.6 * height;

  float midPoint = 0.20;
  float auroraAlpha = smoothstep(midPoint - uBlend * 0.5, midPoint + uBlend * 0.5, intensity);

  vec3 auroraColor = intensity * rampColor;
  fragColor = vec4(auroraColor * auroraAlpha, auroraAlpha);
}
`;

function toColorStops(colors: [string, string, string]) {
  return colors.map((hex) => {
    const c = new Color(hex);
    return [c.r, c.g, c.b];
  });
}

function Aurora({
  className,
  colors = [...DEFAULT_COLORS],
  speed = 1,
  amplitude = 1,
  blend = 0.5,
  children,
  style,
  ...props
}: AuroraProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const propsRef = useRef({ colors, speed, amplitude, blend });
  propsRef.current = { colors, speed, amplitude, blend };

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const renderer = new Renderer({
      alpha: true,
      premultipliedAlpha: true,
      antialias: true,
    });
    const gl = renderer.gl;
    gl.clearColor(0, 0, 0, 0);
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
    gl.canvas.style.backgroundColor = "transparent";
    gl.canvas.style.position = "absolute";
    gl.canvas.style.inset = "0";
    gl.canvas.style.width = "100%";
    gl.canvas.style.height = "100%";
    gl.canvas.style.display = "block";
    gl.canvas.style.pointerEvents = "none";

    const geometry = new Triangle(gl);
    if (geometry.attributes.uv) {
      delete geometry.attributes.uv;
    }

    const program = new Program(gl, {
      vertex: VERT,
      fragment: FRAG,
      uniforms: {
        uTime: { value: 0 },
        uAmplitude: { value: amplitude },
        uColorStops: { value: toColorStops(colors) },
        uResolution: {
          value: [container.offsetWidth, container.offsetHeight],
        },
        uBlend: { value: blend },
      },
    });

    const mesh = new Mesh(gl, { geometry, program });
    container.appendChild(gl.canvas);

    function resize() {
      if (!container) return;
      const width = container.offsetWidth;
      const height = container.offsetHeight;
      renderer.setSize(width, height);
      program.uniforms.uResolution.value = [width, height];
    }

    const observer = new ResizeObserver(resize);
    observer.observe(container);
    resize();

    const reduced =
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    let frame = 0;
    const tick = (t: number) => {
      frame = requestAnimationFrame(tick);
      const current = propsRef.current;
      program.uniforms.uTime.value = reduced
        ? 0
        : t * 0.01 * current.speed * 0.095;
      program.uniforms.uAmplitude.value = current.amplitude;
      program.uniforms.uBlend.value = current.blend;
      program.uniforms.uColorStops.value = toColorStops(current.colors);
      renderer.render({ scene: mesh });
    };
    frame = requestAnimationFrame(tick);

    return () => {
      cancelAnimationFrame(frame);
      observer.disconnect();
      if (gl.canvas.parentNode === container) {
        container.removeChild(gl.canvas);
      }
      gl.getExtension("WEBGL_lose_context")?.loseContext();
    };
  }, []);

  return (
    <div
      ref={containerRef}
      data-slot="aurora"
      className={cn(
        "relative isolate overflow-hidden bg-neutral-950 text-white",
        className,
      )}
      style={style}
      {...props}
    >
      {children ? <div className="relative z-10">{children}</div> : null}
    </div>
  );
}

export { Aurora, DEFAULT_COLORS as AURORA_DEFAULT_COLORS };
```
