ShadersGradient Backgroundsno dependencies

AuroraRibbons

Translucent aurora curtains drawn as SVG bands whose edges follow layered sine waves, flowing continuously over a starry night sky with adjustable intensity, blur and tilt.

Preview

Aurora Ribbons

Translucent curtains of light over a quiet night sky.

Customize

10 props
1
0.9
18
-8
360
20

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add aurora-ribbons

Writes components/ui/aurora-ribbons.tsx and components/styles/aurora-ribbons.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { AuroraRibbons } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <AuroraRibbons />6  );7}

Props

PropTypeDefault
color1

Primary ribbon color.

string'#3dffa8'
color2

Secondary ribbon color.

string'#22d3ee'
color3

Tertiary ribbon color.

string'#a855f7'
backgroundColor

Night-sky color behind the ribbons.

string'#040714'
speed

Flow speed multiplier (0 pauses).

number1
intensity

Ribbon brightness / opacity (0–1.5).

number0.9
blur

Blur applied to the ribbons in px.

number18
angle

Tilt of the ribbon field in degrees.

number-8
height

Height of the background in px.

number360
borderRadius

Corner radius in px.

number20
children

Content rendered centered on top; defaults to a heading and subline.

React.ReactNodeundefined
className

Additional class merged onto the root element.

string''
style

Inline styles merged onto the root element.

React.CSSPropertiesundefined

Accessibility

The star field, glow and SVG ribbons are aria-hidden (the SVG is focusable=false) with pointer-events: none. The requestAnimationFrame loop runs only while visible and speed > 0; prefers-reduced-motion shows a static frame with no star twinkle.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/aurora-ribbons.tsx
1"use client";2 3import * as React from "react";4import styles from "../styles/aurora-ribbons.module.css";5 6export interface AuroraRibbonsProps {7  /** Primary ribbon color */8  color1?: string;9  /** Secondary ribbon color */10  color2?: string;11  /** Tertiary ribbon color */12  color3?: string;13  /** Night-sky color behind the ribbons */14  backgroundColor?: string;15  /** Animation speed multiplier (1 = default flow, 0 = paused) */16  speed?: number;17  /** Ribbon brightness / opacity (0–1.5) */18  intensity?: number;19  /** Blur applied to the ribbons, in px */20  blur?: number;21  /** Tilt of the ribbon field in degrees */22  angle?: number;23  /** Height of the background in px */24  height?: number;25  /** Corner radius in px */26  borderRadius?: number;27  /** Foreground content rendered centered on top */28  children?: React.ReactNode;29  /** Optional className merged onto the root */30  className?: string;31  /** Inline styles merged onto the root */32  style?: React.CSSProperties;33}34 35const VB_W = 1200;36const VB_H = 500;37const SEGMENTS = 48;38 39/** Each ribbon: base line (fraction of height), curtain height, wave frequencies, phase, flow speed, color index */40const RIBBONS = [41  { base: 0.5, tall: 190, f1: 2.1, f2: 4.7, a1: 46, a2: 16, phase: 0.0, flow: 0.32, color: 0 },42  { base: 0.6, tall: 150, f1: 1.6, f2: 5.3, a1: 38, a2: 14, phase: 1.9, flow: 0.26, color: 1 },43  { base: 0.42, tall: 120, f1: 2.7, f2: 3.9, a1: 30, a2: 18, phase: 3.4, flow: 0.38, color: 2 },44  { base: 0.66, tall: 110, f1: 1.3, f2: 6.1, a1: 26, a2: 10, phase: 4.6, flow: 0.22, color: 0 },45  { base: 0.36, tall: 90, f1: 3.1, f2: 4.2, a1: 22, a2: 12, phase: 5.8, flow: 0.42, color: 1 },46];47 48const rand = (n: number) => {49  const x = Math.sin(n * 9301 + 49297) * 233280;50  return x - Math.floor(x);51};52 53const STARS = Array.from({ length: 46 }, (_, i) => ({54  x: rand(i) * 100,55  y: rand(i + 101) * 70,56  r: 0.6 + rand(i + 203) * 1.1,57  o: 0.25 + rand(i + 307) * 0.6,58  d: 2 + rand(i + 409) * 4,59}));60 61function ribbonPath(index: number, t: number): string {62  const r = RIBBONS[index];63  const baseY = r.base * VB_H;64  const top: string[] = [];65  const bottom: string[] = [];66  for (let s = 0; s <= SEGMENTS; s++) {67    const u = s / SEGMENTS;68    const x = -100 + u * (VB_W + 200);69    const y =70      baseY +71      Math.sin(u * r.f1 * Math.PI + t * r.flow + r.phase) * r.a1 +72      Math.sin(u * r.f2 * Math.PI - t * r.flow * 1.6 + r.phase * 0.7) * r.a2;73    const h =74      r.tall *75      (0.55 + 0.45 * Math.sin(u * 3.3 * Math.PI + t * r.flow * 0.8 + r.phase * 1.3)) *76      (0.75 + 0.25 * Math.sin(u * 7.1 * Math.PI - t * r.flow * 1.2));77    bottom.push(`${x.toFixed(1)},${y.toFixed(1)}`);78    top.push(`${x.toFixed(1)},${(y - h).toFixed(1)}`);79  }80  return `M${bottom.join(" L")} L${top.reverse().join(" L")} Z`;81}82 83function useReducedMotionPref() {84  const [reduced, setReduced] = React.useState(false);85  React.useEffect(() => {86    if (typeof window === "undefined" || !window.matchMedia) return;87    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");88    const update = () => setReduced(mq.matches);89    update();90    mq.addEventListener("change", update);91    return () => mq.removeEventListener("change", update);92  }, []);93  return reduced;94}95 96function useInView(ref: React.RefObject<HTMLElement | null>) {97  const [inView, setInView] = React.useState(true);98  React.useEffect(() => {99    const el = ref.current;100    if (!el || typeof IntersectionObserver === "undefined") return;101    const io = new IntersectionObserver((entries) => {102      const entry = entries[entries.length - 1];103      if (entry) setInView(entry.isIntersecting);104    }, { rootMargin: "80px" });105    io.observe(el);106    return () => io.disconnect();107  }, [ref]);108  return inView;109}110 111export function AuroraRibbons({112  color1 = "#3dffa8",113  color2 = "#22d3ee",114  color3 = "#a855f7",115  backgroundColor = "#040714",116  speed = 1,117  intensity = 0.9,118  blur = 18,119  angle = -8,120  height = 360,121  borderRadius = 20,122  children,123  className = "",124  style,125}: AuroraRibbonsProps) {126  const rootRef = React.useRef<HTMLDivElement>(null);127  const pathRefs = React.useRef<Array<SVGPathElement | null>>([]);128  const timeRef = React.useRef(0);129  const reduced = useReducedMotionPref();130  const inView = useInView(rootRef);131  const uid = React.useId().replace(/[^a-zA-Z0-9_-]/g, "");132  const colors = [color1, color2, color3];133  const safeSpeed = Math.max(0, speed);134  const running = !reduced && safeSpeed > 0 && inView;135 136  React.useEffect(() => {137    if (!running) return;138    let raf = 0;139    let last = performance.now();140    const tick = (now: number) => {141      const dt = Math.min(0.05, (now - last) / 1000);142      last = now;143      timeRef.current += dt * safeSpeed;144      for (let i = 0; i < RIBBONS.length; i++) {145        const p = pathRefs.current[i];146        if (p) p.setAttribute("d", ribbonPath(i, timeRef.current));147      }148      raf = requestAnimationFrame(tick);149    };150    raf = requestAnimationFrame(tick);151    return () => cancelAnimationFrame(raf);152  }, [running, safeSpeed]);153 154  const styleVars = {155    "--mf-ar-bg": backgroundColor,156    "--mf-ar-blur": `${Math.max(0, blur)}px`,157    "--mf-ar-intensity": String(Math.max(0, Math.min(1.5, intensity))),158    "--mf-ar-angle": `${angle}deg`,159    "--mf-ar-glow": color1,160    "--mf-ar-state": running ? "running" : "paused",161    height: `${height}px`,162    borderRadius: `${borderRadius}px`,163    ...style,164  } as React.CSSProperties;165 166  return (167    <div ref={rootRef} style={styleVars} className={`${styles.root} ${className}`.trim()}>168      <div aria-hidden="true" className={styles.stars}>169        {STARS.map((s, i) => (170          <span171            key={i}172            className={styles.star}173            style={{174              left: `${s.x.toFixed(2)}%`,175              top: `${s.y.toFixed(2)}%`,176              width: `${s.r.toFixed(2)}px`,177              height: `${s.r.toFixed(2)}px`,178              opacity: Number(s.o.toFixed(2)),179              animationDuration: `${s.d.toFixed(2)}s`,180            }}181          />182        ))}183      </div>184      <div aria-hidden="true" className={styles.glow} />185      <svg186        aria-hidden="true"187        className={styles.ribbons}188        viewBox={`0 0 ${VB_W} ${VB_H}`}189        preserveAspectRatio="none"190        focusable="false"191      >192        <defs>193          {colors.map((c, i) => (194            <linearGradient key={i} id={`${uid}-ar-${i}`} x1="0" y1="0" x2="0" y2="1">195              <stop offset="0%" stopColor={c} stopOpacity="0" />196              <stop offset="50%" stopColor={c} stopOpacity="0.45" />197              <stop offset="88%" stopColor={c} stopOpacity="1" />198              <stop offset="100%" stopColor={c} stopOpacity="0.15" />199            </linearGradient>200          ))}201        </defs>202        {RIBBONS.map((r, i) => (203          <path204            key={i}205            ref={(el) => {206              pathRefs.current[i] = el;207            }}208            className={styles.ribbon}209            d={ribbonPath(i, 0)}210            fill={`url(#${uid}-ar-${r.color})`}211            style={{ opacity: i < 3 ? 1 : 0.6 }}212          />213        ))}214      </svg>215      <div aria-hidden="true" className={styles.horizon} />216      <div className={styles.content}>217        {children ?? (218          <div className={styles.copy}>219            <h3 className={styles.title}>Aurora Ribbons</h3>220            <p className={styles.subtitle}>Translucent curtains of light over a quiet night sky.</p>221          </div>222        )}223      </div>224    </div>225  );226}