Backgrounds
CosmicDust
Fine dust and a few soft out-of-focus specks float upward, swaying and flickering. Pauses off screen.
Download
- React
- Tailwind CSS v4
components/ui/cosmic-dust.tsx
"use client";
import { useEffect, useRef, type ReactNode } from "react";
type CosmicDustProps = {
children?: ReactNode;
className?: string;
/** Colour of the fine dust. */
color?: string;
/** Colour of the large soft specks. */
accent?: string;
background?: string;
/** Number of specks. */
count?: number;
/** 1 is the default pace; 2 is twice as fast. */
speed?: number;
};
type Speck = { x: number; y: number; r: number; vy: number; phase: number; alpha: number; big: boolean };
/* CosmicDust: fine dust and a few soft out-of-focus specks float upward, swaying and flickering. */
export function CosmicDust({
children,
className = "",
color = "#e9e3ff",
accent = "#a78bfa",
background = "#07060f",
count = 220,
speed = 1,
}: CosmicDustProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
if (!canvas || !ctx) return;
const still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let w = 0;
let h = 0;
let raf = 0;
let last = 0;
let time = 0;
let specks: Speck[] = [];
const draw = () => {
ctx.clearRect(0, 0, w, h);
for (const s of specks) {
ctx.fillStyle = s.big ? accent : color;
ctx.globalAlpha = s.alpha * (0.6 + 0.4 * Math.sin(time * 2 + s.phase));
ctx.beginPath();
ctx.arc(s.x + Math.sin(time * 0.5 + s.phase) * 10, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
};
const frame = (now: number) => {
const dt = last ? Math.min((now - last) / 1000, 0.05) * speed : 0;
last = now;
time += dt;
for (const s of specks) {
s.y -= s.vy * dt;
if (s.y < -s.r * 2) {
s.y = h + s.r * 2;
s.x = Math.random() * w;
}
}
draw();
raf = requestAnimationFrame(frame);
};
const start = () => {
if (still || raf) return;
last = 0;
raf = requestAnimationFrame(frame);
};
const stop = () => {
cancelAnimationFrame(raf);
raf = 0;
};
const resize = () => {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
w = canvas.clientWidth;
h = canvas.clientHeight;
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(h * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
specks = Array.from({ length: count }, () => {
const big = Math.random() < 0.08;
return {
x: Math.random() * w,
y: Math.random() * h,
r: big ? 3 + Math.random() * 4 : 0.5 + Math.random() * 1.1,
vy: big ? 4 + Math.random() * 6 : 8 + Math.random() * 16,
phase: Math.random() * Math.PI * 2,
alpha: big ? 0.15 : 0.4 + Math.random() * 0.5,
big,
};
});
draw();
};
// Redraw on size change; only animate while on screen.
const ro = new ResizeObserver(resize);
const io = new IntersectionObserver(([e]) => (e.isIntersecting ? start() : stop()));
ro.observe(canvas);
io.observe(canvas);
return () => {
stop();
ro.disconnect();
io.disconnect();
};
}, [color, accent, count, speed]);
return (
<div className={`relative overflow-hidden ${className}`} style={{ background }}>
<canvas ref={canvasRef} aria-hidden className="absolute inset-0 h-full w-full" />
<div className="relative h-full">{children}</div>
</div>
);
}
1. Paste the Code tab into
components/ui/cosmic-dust.tsx. The styles are Tailwind CSS v4 classes.2. Use it. This is the preview above:
app/cosmic-dust-demo.tsx
import { CosmicDust } from "@/components/ui/cosmic-dust"; export default function CosmicDustExample() { return ( <CosmicDust className="h-full"> <p className="absolute bottom-5 left-6 text-2xl font-medium text-white/90">Afterglow</p> </CosmicDust> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| children | ReactNode | — | Content shown over the background. |
| color | string | "#e9e3ff" | Colour of the fine dust. |
| accent | string | "#a78bfa" | Colour of the large soft specks. |
| background | string | "#07060f" | Base colour. |
| count | number | 220 | Number of specks. |
| speed | number | 1 | 1 is the default pace; 2 is twice as fast. |
| className | string | "" | Give it a height here. |