ShadersGradient Backgroundsno dependencies

NebulaGradient

A raw WebGL fragment shader that layers three passes of domain-warped fbm into glowing nebula clouds with dark dust lanes over near-black space, plus an optional field of softly twinkling stars. Falls back to blurred, drifting CSS gradients when WebGL is unavailable.

Preview

Nebula Gradient

Domain-warped star clouds drifting through deep space.

Customize

8 props
1
1
1

Installation

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

npx motionfoundry add nebula-gradient

Writes components/ui/nebula-gradient.tsx and components/styles/nebula-gradient.module.css.

Usage

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

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

Props

PropTypeDefault
color1

Primary cloud color (hex).

string'#7c3aed'
color2

Secondary cloud color (hex).

string'#ec4899'
color3

Accent color for the brightest filaments (hex).

string'#22d3ee'
backgroundColor

Deep-space base color behind the clouds (hex).

string'#05040b'
speed

Drift speed multiplier (0 pauses).

number1
density

Cloud density and detail, 0.2 (sparse wisps) to 2 (thick nebula).

number1
stars

Render a subtle field of twinkling stars.

booleantrue
intensity

Overall glow brightness (0.2–2).

number1
height

Height of the background in px.

number380
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 canvas (or CSS fallback) is an aria-hidden decorative layer; children render above in normal flow with light, shadowed text for contrast on the dark field. The render loop stops off-screen, at speed 0, and under prefers-reduced-motion (a single static frame is drawn). GL resources are deleted on unmount and a lost context switches to the CSS fallback.

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

Source

components/ui/nebula-gradient.tsx
1"use client";2 3import * as React from "react";4import styles from "../styles/nebula-gradient.module.css";5 6export interface NebulaGradientProps {7  /** Primary cloud color */8  color1?: string;9  /** Secondary cloud color */10  color2?: string;11  /** Accent color for the brightest filaments */12  color3?: string;13  /** Deep-space base color behind the clouds */14  backgroundColor?: string;15  /** Drift speed multiplier (1 = default, 0 = paused) */16  speed?: number;17  /** Cloud density / detail (0.2 = sparse wisps, 2 = thick nebula) */18  density?: number;19  /** Render a subtle field of twinkling stars */20  stars?: boolean;21  /** Overall glow brightness (0.2–2) */22  intensity?: 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 35type RGB = [number, number, number];36type UniformMap = Record<string, number | readonly number[]>;37 38const FRAG = `39#ifdef GL_FRAGMENT_PRECISION_HIGH40precision highp float;41#else42precision mediump float;43#endif44uniform vec2 u_res; uniform float u_time;45uniform vec3 u_c1; uniform vec3 u_c2; uniform vec3 u_c3; uniform vec3 u_bg;46uniform float u_density; uniform float u_stars; uniform float u_intensity;47float hash(vec2 p){ p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }48float noise(vec2 p){ vec2 i = floor(p), f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f);49  return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y); }50float fbm(vec2 p){ float v = 0.0, a = 0.5; mat2 m = mat2(1.6, 1.2, -1.2, 1.6);51  for (int i = 0; i < 5; i++){ v += a * noise(p); p = m * p; a *= 0.5; } return v; }52void main(){53  vec2 uv = (gl_FragCoord.xy - 0.5 * u_res) / u_res.y;54  float t = u_time * 0.05;55  vec2 p = uv * (1.4 + u_density * 0.9);56  vec2 q = vec2(fbm(p + vec2(0.0, t)), fbm(p + vec2(5.2, 1.3) - t));57  vec2 r = vec2(fbm(p + 3.0 * q + vec2(1.7, 9.2) + t * 1.5), fbm(p + 3.0 * q + vec2(8.3, 2.8) - t));58  float f = fbm(p + 2.6 * r);59  float d = smoothstep(0.42 - 0.14 * u_density, 0.98, f);60  vec3 col = mix(u_c1, u_c2, clamp(length(q) * 1.15 - 0.2, 0.0, 1.0));61  col = mix(col, u_c3, clamp(r.x * r.x * 1.8 - 0.15, 0.0, 1.0));62  vec3 neb = col * d * d * 2.2 + col * pow(d, 5.0) * 1.6 + vec3(1.0) * pow(d, 9.0) * 0.5;63  float dust = smoothstep(0.48, 0.78, fbm(p * 1.9 - r * 1.4 + t));64  neb *= (1.0 - dust * 0.6) * u_intensity;65  vec3 c = u_bg + (1.0 - exp(-neb * 1.3));66  if (u_stars > 0.5) {67    vec2 g = gl_FragCoord.xy / u_res.y * 44.0;68    vec2 id = floor(g); vec2 fp = fract(g) - 0.5;69    float h = hash(id);70    vec2 off = (vec2(hash(id + 3.1), hash(id + 7.7)) - 0.5) * 0.6;71    float tw = 0.55 + 0.45 * sin(u_time * (1.2 + h * 2.5) + h * 40.0);72    float s = smoothstep(0.13, 0.0, length(fp - off)) * step(0.9, h) * (h - 0.88) * 9.0;73    c += vec3(0.85, 0.9, 1.0) * s * tw * (1.0 - d * 0.75);74  }75  c *= 1.0 - 0.3 * dot(uv, uv);76  gl_FragColor = vec4(c, 1.0);77}`;78 79const VERT = "attribute vec2 a_pos;void main(){gl_Position=vec4(a_pos,0.0,1.0);}";80 81function hexRgb(hex: string, fb: RGB): RGB {82  const m = hex.trim().replace(/^#/, "");83  const full = m.length === 3 ? m.split("").map((ch) => ch + ch).join("") : m;84  if (!/^[0-9a-fA-F]{6}$/.test(full)) return fb;85  const n = parseInt(full, 16);86  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];87}88 89const rgba = (c: RGB, a: number) => `rgba(${Math.round(c[0] * 255)},${Math.round(c[1] * 255)},${Math.round(c[2] * 255)},${a})`;90 91function useReducedMotionPref() {92  const [reduced, setReduced] = React.useState(false);93  React.useEffect(() => {94    if (typeof window === "undefined" || !window.matchMedia) return;95    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");96    const update = () => setReduced(mq.matches);97    update();98    mq.addEventListener("change", update);99    return () => mq.removeEventListener("change", update);100  }, []);101  return reduced;102}103 104function useInView(ref: React.RefObject<HTMLElement | null>) {105  const [inView, setInView] = React.useState(true);106  React.useEffect(() => {107    const el = ref.current;108    if (!el || typeof IntersectionObserver === "undefined") return;109    const io = new IntersectionObserver((entries) => {110      const entry = entries[entries.length - 1];111      if (entry) setInView(entry.isIntersecting);112    }, { rootMargin: "80px" });113    io.observe(el);114    return () => io.disconnect();115  }, [ref]);116  return inView;117}118 119function compile(gl: WebGLRenderingContext, type: number, src: string) {120  const sh = gl.createShader(type);121  if (!sh) return null;122  gl.shaderSource(sh, src);123  gl.compileShader(sh);124  if (gl.getShaderParameter(sh, gl.COMPILE_STATUS)) return sh;125  gl.deleteShader(sh);126  return null;127}128 129/** Full-screen-triangle WebGL renderer; returns true when the CSS fallback should be shown instead */130function useShader(131  rootRef: React.RefObject<HTMLDivElement | null>,132  canvasRef: React.RefObject<HTMLCanvasElement | null>,133  frag: string,134  uniforms: () => UniformMap,135  running: boolean,136  speed: number,137  paramsKey: string,138  maxDpr = 2,139) {140  const [fallback, setFallback] = React.useState(false);141  const timeRef = React.useRef(12);142  const drawRef = React.useRef<(() => void) | null>(null);143  const uniRef = React.useRef(uniforms);144  uniRef.current = uniforms;145  const speedRef = React.useRef(speed);146  speedRef.current = speed;147 148  React.useEffect(() => {149    const root = rootRef.current;150    const canvas = canvasRef.current;151    if (!root || !canvas || fallback) return;152    const gl = canvas.getContext("webgl", { alpha: false, antialias: false, depth: false, stencil: false, powerPreference: "low-power" });153    if (!gl) { setFallback(true); return; }154    const vs = compile(gl, gl.VERTEX_SHADER, VERT);155    const fs = compile(gl, gl.FRAGMENT_SHADER, frag);156    const prog = gl.createProgram();157    const buf = gl.createBuffer();158    const dispose = () => {159      gl.deleteBuffer(buf);160      gl.deleteProgram(prog);161      gl.deleteShader(vs);162      gl.deleteShader(fs);163    };164    if (!vs || !fs || !prog || !buf) { dispose(); setFallback(true); return; }165    gl.attachShader(prog, vs);166    gl.attachShader(prog, fs);167    gl.linkProgram(prog);168    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { dispose(); setFallback(true); return; }169    gl.useProgram(prog);170    gl.bindBuffer(gl.ARRAY_BUFFER, buf);171    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);172    const aPos = gl.getAttribLocation(prog, "a_pos");173    gl.enableVertexAttribArray(aPos);174    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);175    const locs = new Map<string, WebGLUniformLocation | null>();176    const loc = (n: string) => {177      if (!locs.has(n)) locs.set(n, gl.getUniformLocation(prog, n));178      return locs.get(n) ?? null;179    };180    const resize = () => {181      const rect = root.getBoundingClientRect();182      const dpr = Math.min(maxDpr, window.devicePixelRatio || 1);183      canvas.width = Math.max(1, Math.round(rect.width * dpr));184      canvas.height = Math.max(1, Math.round(rect.height * dpr));185      gl.viewport(0, 0, canvas.width, canvas.height);186    };187    const draw = () => {188      if (gl.isContextLost()) return;189      gl.uniform2f(loc("u_res"), canvas.width, canvas.height);190      gl.uniform1f(loc("u_time"), timeRef.current);191      const u = uniRef.current();192      for (const k in u) {193        const v = u[k];194        if (typeof v === "number") gl.uniform1f(loc(k), v);195        else if (v.length === 2) gl.uniform2f(loc(k), v[0], v[1]);196        else gl.uniform3f(loc(k), v[0], v[1], v[2]);197      }198      gl.drawArrays(gl.TRIANGLES, 0, 3);199    };200    resize();201    draw();202    drawRef.current = draw;203    const ro = new ResizeObserver(() => { resize(); draw(); });204    ro.observe(root);205    const onLost = (e: Event) => { e.preventDefault(); drawRef.current = null; setFallback(true); };206    canvas.addEventListener("webglcontextlost", onLost);207    return () => {208      ro.disconnect();209      canvas.removeEventListener("webglcontextlost", onLost);210      drawRef.current = null;211      if (!gl.isContextLost()) dispose();212    };213  }, [frag, fallback, maxDpr, rootRef, canvasRef]);214 215  React.useEffect(() => {216    if (!running || fallback) return;217    let raf = 0;218    let last = performance.now();219    const tick = (now: number) => {220      const dt = Math.min(0.05, (now - last) / 1000);221      last = now;222      timeRef.current += dt * speedRef.current;223      drawRef.current?.();224      raf = requestAnimationFrame(tick);225    };226    raf = requestAnimationFrame(tick);227    return () => cancelAnimationFrame(raf);228  }, [running, fallback]);229 230  React.useEffect(() => { drawRef.current?.(); }, [paramsKey]);231 232  return fallback;233}234 235const rand = (n: number) => {236  const x = Math.sin(n * 9301 + 49297) * 233280;237  return x - Math.floor(x);238};239 240export function NebulaGradient({241  color1 = "#7c3aed",242  color2 = "#ec4899",243  color3 = "#22d3ee",244  backgroundColor = "#05040b",245  speed = 1,246  density = 1,247  stars = true,248  intensity = 1,249  height = 380,250  borderRadius = 20,251  children,252  className = "",253  style,254}: NebulaGradientProps) {255  const rootRef = React.useRef<HTMLDivElement>(null);256  const canvasRef = React.useRef<HTMLCanvasElement>(null);257  const reduced = useReducedMotionPref();258  const inView = useInView(rootRef);259  const safeSpeed = Math.max(0, speed);260  const safeDensity = Math.max(0.2, Math.min(2, density));261  const safeIntensity = Math.max(0.2, Math.min(2, intensity));262  const c1 = hexRgb(color1, [0.49, 0.23, 0.93]);263  const c2 = hexRgb(color2, [0.93, 0.28, 0.6]);264  const c3 = hexRgb(color3, [0.13, 0.83, 0.93]);265  const bg = hexRgb(backgroundColor, [0.02, 0.016, 0.043]);266 267  const fallback = useShader(268    rootRef,269    canvasRef,270    FRAG,271    () => ({ u_c1: c1, u_c2: c2, u_c3: c3, u_bg: bg, u_density: safeDensity, u_stars: stars ? 1 : 0, u_intensity: safeIntensity }),272    !reduced && inView && safeSpeed > 0,273    safeSpeed,274    [color1, color2, color3, backgroundColor, safeDensity, stars, safeIntensity].join("|"),275    1.5,276  );277 278  const starField = React.useMemo(279    () =>280      Array.from({ length: 28 }, (_, i) => `radial-gradient(1px 1px at ${(rand(i) * 100).toFixed(1)}% ${(rand(i + 50) * 100).toFixed(1)}%, rgba(235,240,255,${(0.4 + rand(i + 9) * 0.6).toFixed(2)}), transparent)`).join(","),281    [],282  );283 284  const styleVars = {285    "--mf-nb-bg": backgroundColor,286    "--mf-nb-clouds": `radial-gradient(38% 46% at 30% 42%, ${rgba(c1, 0.75 * safeIntensity)}, transparent 70%), radial-gradient(34% 40% at 66% 58%, ${rgba(c2, 0.6 * safeIntensity)}, transparent 70%), radial-gradient(22% 26% at 52% 36%, ${rgba(c3, 0.45 * safeIntensity)}, transparent 70%)`,287    "--mf-nb-stars": starField,288    "--mf-nb-dur": `${(28 / Math.max(0.1, safeSpeed)).toFixed(1)}s`,289    height: `${height}px`,290    borderRadius: `${borderRadius}px`,291    ...style,292  } as React.CSSProperties;293 294  return (295    <div ref={rootRef} style={styleVars} className={`${styles.root} ${className}`.trim()}>296      {fallback ? (297        <div aria-hidden="true" className={`${styles.fallback} ${safeSpeed === 0 ? styles.paused : ""}`.trim()}>298          <div className={styles.clouds} />299          {stars ? <div className={styles.stars} /> : null}300        </div>301      ) : (302        <canvas ref={canvasRef} aria-hidden="true" className={styles.canvas} />303      )}304      <div className={styles.content}>305        {children ?? (306          <div className={styles.copy}>307            <h3 className={styles.title}>Nebula Gradient</h3>308            <p className={styles.subtitle}>Domain-warped star clouds drifting through deep space.</p>309          </div>310        )}311      </div>312    </div>313  );314}