Backgrounds
QuantumDots
Drifting dots link up with fading lines whenever they come close. Pauses off screen.
Download
- React
- Tailwind CSS v4
components/ui/quantum-dots.tsx
"use client";
import { useEffect, useRef, type ReactNode } from "react";
type QuantumDotsProps = {
children?: ReactNode;
className?: string;
color?: string;
background?: string;
/** Number of dots. */
count?: number;
/** Dots closer than this, in pixels, are linked by a line. */
link?: number;
/** 1 is the default pace; 2 is twice as fast. */
speed?: number;
};
type Point = { x: number; y: number; vx: number; vy: number };
/* QuantumDots: drifting dots that link up with fading lines whenever they come close. */
export function QuantumDots({
children,
className = "",
color = "#a78bfa",
background = "#06050d",
count = 70,
link = 110,
speed = 1,
}: QuantumDotsProps) {
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 nodes: Point[] = [];
const draw = () => {
ctx.clearRect(0, 0, w, h);
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 1;
for (let i = 0; i < nodes.length; i++) {
const a = nodes[i];
for (let j = i + 1; j < nodes.length; j++) {
const b = nodes[j];
const d = Math.hypot(a.x - b.x, a.y - b.y);
if (d >= link) continue;
ctx.globalAlpha = (1 - d / link) * 0.5;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
}
ctx.globalAlpha = 0.9;
for (const n of nodes) {
ctx.beginPath();
ctx.arc(n.x, n.y, 1.6, 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;
for (const n of nodes) {
n.x += n.vx * dt;
n.y += n.vy * dt;
if (n.x < 0 || n.x > w) n.vx *= -1;
if (n.y < 0 || n.y > h) n.vy *= -1;
}
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);
nodes = Array.from({ length: count }, () => ({
x: Math.random() * w,
y: Math.random() * h,
vx: (Math.random() - 0.5) * 30,
vy: (Math.random() - 0.5) * 30,
}));
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, count, link, 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/quantum-dots.tsx. The styles are Tailwind CSS v4 classes.2. Use it. This is the preview above:
app/quantum-dots-demo.tsx
import { QuantumDots } from "@/components/ui/quantum-dots"; export default function QuantumDotsExample() { return ( <QuantumDots className="h-full"> <p className="absolute bottom-5 left-6 text-2xl font-medium text-white/90">Everything connects</p> </QuantumDots> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| children | ReactNode | — | Content shown over the background. |
| color | string | "#a78bfa" | Dot and line colour. |
| background | string | "#06050d" | Base colour. |
| count | number | 70 | Number of dots. |
| link | number | 110 | Dots closer than this, in pixels, are linked. |
| speed | number | 1 | 1 is the default pace; 2 is twice as fast. |
| className | string | "" | Give it a height here. |