Dots Background
Original · freedotted grid background
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/bgx-dots.json"use client";
import {
useId,
useRef,
useState,
type CSSProperties,
type KeyboardEvent,
type PointerEvent,
} from "react";
import {
motion,
useMotionTemplate,
useMotionValue,
useReducedMotion,
useSpring,
} from "motion/react";
type Accent = {
id: string;
label: string;
dot: string;
swatch: string;
};
const ACCENTS: readonly Accent[] = [
{ id: "indigo", label: "Indigo", dot: "#6366f1", swatch: "bg-indigo-500" },
{ id: "violet", label: "Violet", dot: "#8b5cf6", swatch: "bg-violet-500" },
{ id: "sky", label: "Sky", dot: "#0ea5e9", swatch: "bg-sky-500" },
{ id: "emerald", label: "Emerald", dot: "#10b981", swatch: "bg-emerald-500" },
{ id: "amber", label: "Amber", dot: "#f59e0b", swatch: "bg-amber-500" },
{ id: "rose", label: "Rose", dot: "#f43f5e", swatch: "bg-rose-500" },
] as const;
const EDGE_MASK =
"radial-gradient(ellipse 115% 100% at 50% 46%, #000 52%, transparent 100%)";
function Switch({
checked,
onChange,
label,
}: {
checked: boolean;
onChange: (next: boolean) => void;
label: string;
}) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
aria-label={label}
onClick={() => onChange(!checked)}
className={[
"relative inline-flex h-6 w-11 shrink-0 items-center rounded-full",
"outline-none transition-colors duration-200",
"focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2",
"focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950",
checked
? "bg-indigo-500"
: "bg-slate-300 dark:bg-slate-700",
].join(" ")}
>
<span
className={[
"inline-block h-4 w-4 rounded-full bg-white shadow-sm",
"transition-transform duration-200",
checked ? "translate-x-6" : "translate-x-1",
].join(" ")}
/>
</button>
);
}
export default function BgxDots() {
const reduce = useReducedMotion();
const sectionRef = useRef<HTMLElement | null>(null);
const swatchRefs = useRef<Array<HTMLButtonElement | null>>([]);
const gapId = useId();
const sizeId = useId();
const spotId = useId();
const [gap, setGap] = useState(26);
const [dotSize, setDotSize] = useState(1.6);
const [spotSize, setSpotSize] = useState(190);
const [accentId, setAccentId] = useState<string>("indigo");
const [spotOn, setSpotOn] = useState(true);
const [driftOn, setDriftOn] = useState(true);
const accent = ACCENTS.find((a) => a.id === accentId) ?? ACCENTS[0];
const mx = useMotionValue(50);
const my = useMotionValue(50);
const sx = useSpring(mx, { stiffness: 140, damping: 22, mass: 0.6 });
const sy = useSpring(my, { stiffness: 140, damping: 22, mass: 0.6 });
const tx = reduce ? mx : sx;
const ty = reduce ? my : sy;
const mask = useMotionTemplate`radial-gradient(circle ${spotSize}px at ${tx}% ${ty}%, #000 0%, rgba(0,0,0,0.55) 46%, transparent 74%)`;
const handlePointer = (event: PointerEvent<HTMLElement>) => {
const rect = sectionRef.current?.getBoundingClientRect();
if (!rect) return;
mx.set(((event.clientX - rect.left) / rect.width) * 100);
my.set(((event.clientY - rect.top) / rect.height) * 100);
};
const handleLeave = () => {
mx.set(50);
my.set(50);
};
const handleSwatchKey = (event: KeyboardEvent<HTMLDivElement>) => {
const index = ACCENTS.findIndex((a) => a.id === accentId);
let next = index;
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
next = (index + 1) % ACCENTS.length;
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
next = (index - 1 + ACCENTS.length) % ACCENTS.length;
} else if (event.key === "Home") {
next = 0;
} else if (event.key === "End") {
next = ACCENTS.length - 1;
} else {
return;
}
event.preventDefault();
setAccentId(ACCENTS[next].id);
swatchRefs.current[next]?.focus();
};
const baseStyle = {
["--bgx-gap"]: `${gap}px`,
backgroundImage: `radial-gradient(circle at center, var(--bgx-dot) ${dotSize}px, transparent ${dotSize + 0.7}px)`,
backgroundSize: `${gap}px ${gap}px`,
maskImage: EDGE_MASK,
WebkitMaskImage: EDGE_MASK,
} as CSSProperties;
const accentStyle = {
["--bgx-gap"]: `${gap}px`,
backgroundImage: `radial-gradient(circle at center, ${accent.dot} ${dotSize + 0.5}px, transparent ${dotSize + 1.3}px)`,
backgroundSize: `${gap}px ${gap}px`,
} as CSSProperties;
const glowStyle = {
background: `radial-gradient(60% 55% at 78% 18%, ${accent.dot}22, transparent 70%), radial-gradient(50% 50% at 12% 92%, ${accent.dot}1a, transparent 72%)`,
} as CSSProperties;
return (
<section
ref={sectionRef}
onPointerMove={handlePointer}
onPointerLeave={handleLeave}
aria-label="Dotted grid background demo"
className="relative w-full overflow-hidden bg-white px-6 py-24 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:py-32"
>
<style>{`
.bgxdots-base { --bgx-dot: rgba(71, 85, 105, 0.38); }
@media (prefers-color-scheme: dark) {
.bgxdots-base { --bgx-dot: rgba(148, 163, 184, 0.30); }
}
@keyframes bgxdots-drift {
from { background-position: 0px 0px; }
to { background-position: var(--bgx-gap) var(--bgx-gap); }
}
.bgxdots-animated { animation: bgxdots-drift 18s linear infinite; }
@media (prefers-reduced-motion: reduce) {
.bgxdots-animated { animation: none !important; }
}
`}</style>
{/* soft accent atmosphere */}
<div aria-hidden className="pointer-events-none absolute inset-0" style={glowStyle} />
{/* base dotted grid */}
<div
aria-hidden
className={[
"bgxdots-base pointer-events-none absolute inset-0",
driftOn && !reduce ? "bgxdots-animated" : "",
].join(" ")}
style={baseStyle}
/>
{/* accent spotlight grid following the cursor */}
<motion.div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{ ...accentStyle, maskImage: mask, WebkitMaskImage: mask }}
animate={{ opacity: spotOn ? 1 : 0 }}
transition={{ duration: reduce ? 0 : 0.35, ease: "easeOut" }}
/>
<div className="relative mx-auto grid max-w-5xl gap-12 lg:grid-cols-2 lg:items-center lg:gap-16">
<div>
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-xs font-medium uppercase tracking-[0.18em] text-slate-500 backdrop-blur dark:border-slate-800 dark:bg-slate-900/60 dark:text-slate-400">
<span className={`h-1.5 w-1.5 rounded-full ${accent.swatch}`} />
Backgrounds / Dotted grid
</span>
<h2 className="mt-6 text-4xl font-semibold leading-[1.05] tracking-tight sm:text-5xl">
A dotted grid that
<br className="hidden sm:block" /> tracks the cursor.
</h2>
<p className="mt-5 max-w-md text-base leading-relaxed text-slate-600 dark:text-slate-400">
Drop this behind any hero, pricing table, or dashboard. The grid is
painted with pure CSS radial-gradients, so it stays razor-sharp at
every zoom level and costs almost nothing to render. Move your
pointer across the panel to light up the spotlight.
</p>
<dl className="mt-8 flex flex-wrap gap-x-8 gap-y-4 text-sm">
<div>
<dt className="text-slate-500 dark:text-slate-400">Paint cost</dt>
<dd className="mt-0.5 font-semibold tabular-nums">2 gradients</dd>
</div>
<div>
<dt className="text-slate-500 dark:text-slate-400">Spacing</dt>
<dd className="mt-0.5 font-semibold tabular-nums">{gap}px grid</dd>
</div>
<div>
<dt className="text-slate-500 dark:text-slate-400">DOM nodes</dt>
<dd className="mt-0.5 font-semibold tabular-nums">3 layers</dd>
</div>
</dl>
</div>
<div className="rounded-2xl border border-slate-200 bg-white/75 p-6 shadow-xl shadow-slate-900/5 backdrop-blur-md dark:border-slate-800 dark:bg-slate-900/70 dark:shadow-black/30 sm:p-7">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold tracking-tight">Grid controls</h3>
<span className="rounded-md bg-slate-100 px-2 py-1 text-[11px] font-medium text-slate-500 dark:bg-slate-800 dark:text-slate-400">
live
</span>
</div>
<div className="mt-6 space-y-6">
<div>
<div className="flex items-baseline justify-between">
<label htmlFor={gapId} className="text-sm font-medium">
Dot spacing
</label>
<span className="text-xs tabular-nums text-slate-500 dark:text-slate-400">
{gap}px
</span>
</div>
<input
id={gapId}
type="range"
min={14}
max={44}
step={1}
value={gap}
onChange={(event) => setGap(Number(event.target.value))}
style={{ accentColor: accent.dot }}
className="mt-2 w-full cursor-pointer rounded-full outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
/>
</div>
<div>
<div className="flex items-baseline justify-between">
<label htmlFor={sizeId} className="text-sm font-medium">
Dot size
</label>
<span className="text-xs tabular-nums text-slate-500 dark:text-slate-400">
{dotSize.toFixed(1)}px
</span>
</div>
<input
id={sizeId}
type="range"
min={0.8}
max={3.4}
step={0.2}
value={dotSize}
onChange={(event) => setDotSize(Number(event.target.value))}
style={{ accentColor: accent.dot }}
className="mt-2 w-full cursor-pointer rounded-full outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
/>
</div>
<div>
<div className="flex items-baseline justify-between">
<label htmlFor={spotId} className="text-sm font-medium">
Spotlight radius
</label>
<span className="text-xs tabular-nums text-slate-500 dark:text-slate-400">
{spotSize}px
</span>
</div>
<input
id={spotId}
type="range"
min={90}
max={340}
step={10}
value={spotSize}
disabled={!spotOn}
onChange={(event) => setSpotSize(Number(event.target.value))}
style={{ accentColor: accent.dot }}
className="mt-2 w-full cursor-pointer rounded-full outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-40 dark:focus-visible:ring-offset-slate-900"
/>
</div>
<div>
<span id={`${gapId}-accent`} className="text-sm font-medium">
Accent
</span>
<div
role="radiogroup"
aria-labelledby={`${gapId}-accent`}
onKeyDown={handleSwatchKey}
className="mt-2 flex flex-wrap gap-2"
>
{ACCENTS.map((item, index) => {
const selected = item.id === accentId;
return (
<button
key={item.id}
ref={(node) => {
swatchRefs.current[index] = node;
}}
type="button"
role="radio"
aria-checked={selected}
aria-label={item.label}
tabIndex={selected ? 0 : -1}
onClick={() => setAccentId(item.id)}
className={[
"h-8 w-8 rounded-full outline-none transition-transform",
item.swatch,
"focus-visible:ring-2 focus-visible:ring-offset-2",
"focus-visible:ring-slate-900 focus-visible:ring-offset-white",
"dark:focus-visible:ring-white dark:focus-visible:ring-offset-slate-900",
selected
? "scale-110 ring-2 ring-slate-900 ring-offset-2 ring-offset-white dark:ring-white dark:ring-offset-slate-900"
: "opacity-80 hover:opacity-100",
].join(" ")}
/>
);
})}
</div>
</div>
<div className="space-y-3 border-t border-slate-200 pt-5 dark:border-slate-800">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Cursor spotlight</span>
<Switch checked={spotOn} onChange={setSpotOn} label="Toggle cursor spotlight" />
</div>
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-medium">Ambient drift</span>
{reduce ? (
<p className="text-xs text-slate-500 dark:text-slate-400">
Paused — reduced motion is on
</p>
) : null}
</div>
<Switch checked={driftOn} onChange={setDriftOn} label="Toggle ambient drift animation" />
</div>
</div>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Aurora Blobs Background
OriginalAn animated hero section with soft, ever-drifting aurora gradient blobs floating behind staggered, blur-in content.

Retro Perspective Grid
OriginalA launch and waitlist section set over an infinite neon grid that scrolls into a glowing horizon with rising particles.

Conic Gradient Flow
OriginalA frosted hero backed by twin counter-rotating conic gradients, with flowing gradient text and an animated gradient-border stat card.

Spotlight Follow Background
OriginalA dot-grid section where a spring-eased spotlight follows the cursor and reveals a hidden lattice of coloured dots.

Dot Pattern Background
pattern.tsxA reusable animated background for React and Tailwind: dot pattern background.

Flickering Grid Background
grid.tsxA reusable animated background for React and Tailwind: flickering grid background.

Grid Pattern Background
pattern.tsxA reusable animated background for React and Tailwind: grid pattern background.

Retro Grid Background
grid.jsonA reusable animated background for React and Tailwind: retro grid background.

Aurora Background
Originalaurora gradient background

Mesh Gradient Background
Originalanimated mesh gradient background

Grid Background
Originalgrid line background with fade

Noise Background
Originalnoise/grain gradient background

