Signature
An animated SVG signature effect that draws out text as if hand-written.
Live preview recorded from Mellow UI; component and demo published by Mellow UI. Preview: platform recorded · rights authorized.
An aurora borealis background — noise-driven light curtains ripple and fold across the sky over twinkling stars; on light themes the same curtains render as so…
MSource LandMellow UIWhy it stands out
An aurora borealis background — noise-driven light curtains ripple and fold across the sky over twinkling stars; on light themes the same curtains render as soft pigment veils.
You are given a task to integrate a React component into your codebase.
Please verify your project has the following setup:
- shadcn/ui project structure
- Tailwind CSS v4.0
- TypeScript
- motion/react v11
If any of these are missing, provide instructions on how to setup project via shadcn CLI, install Tailwind or TypeScript.
Determine the default path for components and styles. Mellow components live in `components/mellow/`. If that folder does not exist yet, create it.
Copy-paste these files:
File location: components/aurora-veil-demo.tsx
File content: "use client";
import React from "react";
import { AuroraVeil } from "../mellow/aurora-veil";
export default function AuroraVeilDemo() {
return (
<AuroraVeil className="h-full w-full">
<div className="flex h-full flex-col items-center justify-center gap-4 text-center">
<p className="[font-family:var(--font-mono)] text-[0.625rem] font-medium tracking-[0.24em] text-[rgba(var(--ink-rgb),0.55)] uppercase">
69.6° N, 18.9° E — Tromsø
</p>
<h2 className="max-w-md [font-family:var(--font-serif)] text-4xl leading-tight text-[var(--ink)] italic">
The sky, rehearsing
</h2>
<p className="max-w-sm text-[0.875rem] leading-relaxed text-[rgba(var(--ink-rgb),0.6)]">
Noise-driven curtains of light fold and ripple behind your content,
over a field of twinkling stars.
</p>
</div>
</AuroraVeil>
);
}
File location: components/mellow/aurora-veil.tsx
File content: "use client";
import React, { useEffect, useRef, useState } from "react";
function useInkRgb(): string {
const [rgb, setRgb] = useState<string>("244, 241, 236");
useEffect(() => {
const read = () => {
const v = getComputedStyle(document.documentElement)
.getPropertyValue("--ink-rgb")
.trim();
if (v) setRgb(v);
};
read();
const obs = new MutationObserver(read);
obs.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "style", "data-theme"],
});
return () => obs.disconnect();
}, []);
return rgb;
}
/** True when the page background is light — re-checks on theme change. */
function useIsLight(): boolean {
const [light, setLight] = useState(false);
useEffect(() => {
const read = () => {
const raw = getComputedStyle(document.documentElement)
.getPropertyValue("--background-rgb")
.split(",")
.map((n) => parseFloat(n));
if (raw.length >= 3 && raw.every((n) => !isNaN(n))) {
setLight((raw[0] * 0.299 + raw[1] * 0.587 + raw[2] * 0.114) / 255 > 0.5);
}
};
read();
const obs = new MutationObserver(read);
obs.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "style", "data-theme"],
});
return () => obs.disconnect();
}, []);
return light;
}
function hash(x: number, y: number): number {
const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
return n - Math.floor(n);
}
function vnoise(x: number, y: number): number {
const ix = Math.floor(x), iy = Math.floor(y);
const fx = x - ix, fy = y - iy;
const ux = fx * fx * (3 - 2 * fx);
const uy = fy * fy * (3 - 2 * fy);
const ab = hash(ix, iy) + (hash(ix + 1, iy) - hash(ix, iy)) * ux;
const cd = hash(ix, iy + 1) + (hash(ix + 1, iy + 1) - hash(ix, iy + 1)) * ux;
return ab + (cd - ab) * uy;
}
export interface AuroraVeilProps {
/** Curtain colours as "R, G, B" triplets, one per band. */
colors?: string[];
/** Animation speed multiplier. */
speed?: number;
/** Overall brightness of the curtains, 0–2. */
intensity?: number;
/** Twinkling stars behind the aurora (dark themes only). */
stars?: boolean;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}
// luminous ribbons for night skies
const NIGHT_COLORS = ["120, 235, 175", "95, 185, 235", "175, 130, 240"];
// pigment veils for paper
const DAY_COLORS = ["20, 130, 95", "30, 100, 160", "110, 60, 180"];
/**
* An aurora borealis background — noise-driven light curtains ripple and
* fold across the sky over twinkling stars. On light themes the same
* curtains render as soft pigment veils.
*/
export function AuroraVeil({
colors,
speed = 1,
intensity = 1,
stars = true,
className,
style,
children,
}: AuroraVeilProps) {
const inkRgb = useInkRgb();
const isLight = useIsLight();
const canvasRef = useRef<HTMLCanvasElement>(null);
const sizeRef = useRef({ w: 0, h: 0 });
useEffect(() => {
if (!canvasRef.current) return;
const canvas: HTMLCanvasElement = canvasRef.current;
const ctxOrNull = canvas.getContext("2d");
if (!ctxOrNull) return;
const ctx: CanvasRenderingContext2D = ctxOrNull;
const buf = document.createElement("canvas");
const bctxOrNull = buf.getContext("2d");
if (!bctxOrNull) return;
const bctx: CanvasRenderingContext2D = bctxOrNull;
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const palette = colors ?? (isLight ? DAY_COLORS : NIGHT_COLORS);
const alphaScale = (isLight ? 0.2 : 0.34) * intensity;
let rafId = 0;
let t = 0.7;
let last = 0;
function resize() {
const dpr = Math.min(window.devicePixelRatio, 2);
const rect = canvas.getBoundingClientRect();
sizeRef.current = { w: rect.width, h: rect.height };
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
buf.width = Math.max(2, Math.ceil(rect.width / 4));
buf.height = Math.max(2, Math.ceil(rect.height / 4));
}
function frame() {
const { w, h } = sizeRef.current;
if (w === 0 || h === 0) return;
const bw = buf.width;
const bh = buf.height;
bctx.setTransform(1, 0, 0, 1, 0, 0);
bctx.globalCompositeOperation = "source-over";
bctx.clearRect(0, 0, bw, bh);
bctx.globalCompositeOperation = isLight ? "source-over" : "lighter";
const step = 4;
for (let l = 0; l < palette.length; l++) {
const seed = l * 17.7;
const drift = t * (0.6 + l * 0.25);
// top edge of the curtain
const top: number[] = [];
const len: number[] = [];
const n = Math.ceil(bw / step) + 1;
for (let i = 0; i < n; i++) {
const x = (i * step) / bw;
const band =
vnoise(x * 2.2 + seed, drift) * 0.55 +
vnoise(x * 5.5 + seed + 9, drift * 1.6) * 0.2;
top[i] = bh * (0.05 + l * 0.14) + band * bh * 0.36;
len[i] = bh * (0.28 + vnoise(x * 3.1 + seed + 4, drift * 1.2) * 0.42);
}
// three offset passes fake the vertical ray falloff
for (let pass = 0; pass < 3; pass++) {
const pf = 1 - pass * 0.33;
bctx.beginPath();
bctx.moveTo(0, top[0] + pass * 4);
for (let i = 1; i < n; i++) {
const px = i * step;
const mx = px - step / 2;
bctx.quadraticCurveTo(
mx,
top[i - 1] + pass * 4,
px,
top[i] + pass * 4
);
}
for (let i = n - 1; i >= 0; i--) {
bctx.lineTo(i * step, top[i] + len[i] * pf + pass * 10);
}
bctx.closePath();
const g = bctx.createLinearGradient(0, bh * (0.02 + l * 0.12), 0, bh);
const a = alphaScale * (0.5 - pass * 0.14);
g.addColorStop(0, `rgba(${palette[l]}, 0)`);
g.addColorStop(0.16, `rgba(${palette[l]}, ${a.toFixed(3)})`);
g.addColorStop(0.6, `rgba(${palette[l]}, ${(a * 0.25).toFixed(3)})`);
g.addColorStop(1, `rgba(${palette[l]}, 0)`);
bctx.fillStyle = g;
bctx.fill();
}
}
ctx.clearRect(0, 0, w, h);
if (stars && !isLight) {
for (let i = 0; i < 70; i++) {
const sxr = hash(i, 1.3);
const syr = hash(i, 7.9);
const tw = 0.15 + vnoise(i * 3.3, t * 2.4) * 0.55;
ctx.fillStyle = `rgba(${inkRgb}, ${tw.toFixed(3)})`;
const r = 0.5 + hash(i, 11.1) * 1.1;
ctx.fillRect(sxr * w, syr * h * 0.85, r, r);
}
}
// blurred upscale — the cheap glow
ctx.imageSmoothingEnabled = true;
ctx.filter = "blur(10px)";
ctx.drawImage(buf, -12, -12, w + 24, h + 24);
ctx.filter = "none";
}
function loop(now: number) {
const dt = Math.min((now - last) / 1000, 1 / 20);
last = now;
t += dt * 0.16 * speed;
frame();
rafId = requestAnimationFrame(loop);
}
const ro = new ResizeObserver(() => {
resize();
if (reduced) frame();
});
ro.observe(canvas);
resize();
if (reduced) {
frame();
} else {
last = performance.now();
rafId = requestAnimationFrame(loop);
}
return () => {
cancelAnimationFrame(rafId);
ro.disconnect();
};
}, [colors, speed, intensity, stars, inkRgb, isLight]);
return (
<div
className={["relative overflow-hidden", className].filter(Boolean).join(" ")}
style={style}
>
<canvas
ref={canvasRef}
aria-hidden="true"
className="pointer-events-none absolute inset-0 h-full w-full"
/>
{children && <div className="absolute inset-0 z-[1]">{children}</div>}
</div>
);
}
export default AuroraVeil;
Usage notes:
Add an AuroraVeil component from the mellow library — an aurora borealis background — noise-driven light curtains ripple and fold across the sky over twinkling stars; on light themes the same curtains render as soft pigment veils. Key props: `colors`, `speed`, `intensity`, `stars`, `children`. Copy the file into your project; it is self-contained and respects prefers-reduced-motion.Discovery vocabulary
Related by governed terms
An animated SVG signature effect that draws out text as if hand-written.
More from Mellow UI