Backgrounds
StarDrift
Stars drift at different depths and twinkle. Near stars are bigger, brighter and faster. Pauses off screen.
Download
- React
- Tailwind CSS v4
components/ui/star-drift.tsx
"use client";
import { useEffect, useRef, type ReactNode } from "react";
type StarDriftProps = {
children?: ReactNode;
className?: string;
/** Number of stars. */
count?: number;
color?: string;
background?: string;
/** 1 is the default pace; 2 is twice as fast. */
speed?: number;
};
type Star = { x: number; y: number; z: number; phase: number };
/* StarDrift: a field of stars drifting at different depths. Near stars are bigger, brighter and faster. */
export function StarDrift({
children,
className = "",
count = 160,
color = "#ffffff",
background = "#04050d",
speed = 1,
}: StarDriftProps) {
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 stars: Star[] = [];
const draw = () => {
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = color;
for (const s of stars) {
ctx.globalAlpha = (0.3 + 0.7 * s.z) * (0.75 + 0.25 * Math.sin(time * 3 * s.z + s.phase));
ctx.beginPath();
ctx.arc(s.x, s.y, 0.4 + s.z * 1.3, 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 stars) {
s.x += 14 * s.z * dt;
s.y -= 4 * s.z * dt;
if (s.x > w + 2) s.x = -2;
if (s.y < -2) s.y = h + 2;
}
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);
stars = Array.from({ length: count }, () => ({
x: Math.random() * w,
y: Math.random() * h,
z: 0.15 + Math.random() * 0.85,
phase: Math.random() * Math.PI * 2,
}));
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();
};
}, [count, color, 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/star-drift.tsx. The styles are Tailwind CSS v4 classes.2. Use it. This is the preview above:
app/star-drift-demo.tsx
import { StarDrift } from "@/components/ui/star-drift"; export default function StarDriftExample() { return ( <StarDrift className="h-full"> <p className="absolute bottom-5 left-6 text-2xl font-medium text-white/90">Deep field</p> </StarDrift> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| children | ReactNode | — | Content shown over the field. |
| count | number | 160 | Number of stars. |
| color | string | "#ffffff" | Star colour. |
| background | string | "#04050d" | Base colour. |
| speed | number | 1 | 1 is the default pace; 2 is twice as fast. |
| className | string | "" | Give it a height here. |