Mesmerizing radial dot pattern with rainbow hover effects — add to your site in seconds
Copy URL or Code
Paste to your AI coding assistant and customize:
Done. Your AI handles the rest.
Fully customizable. Move your mouse anywhere inside the circle to light up dots based on angle. Adjacent dots glow for a smooth rainbow trail effect.
/*
================================================================================
AI COMPONENT: DizzyDots
================================================================================
QUICK START FOR AI ASSISTANTS (Claude Code, Cursor, Codex):
1. Create file: components/DizzyDots.tsx
2. Paste this entire code block
3. Import: import DizzyDots from "@/components/DizzyDots"
================================================================================
PROPS - All optional with sensible defaults
================================================================================
| Prop | Type | Default | What it does |
|------------|---------|---------|-------------------------------------|
| size | number | 320 | Container width/height in pixels |
| dotCount | number | 60 | Total dots in circle (like a clock) |
| baseColor | string | "#444" | Inactive dot color |
| dotSize | number | 12 | Diameter of each dot in pixels |
| radius | number | 200 | Distance from center to dots |
| showIndex | boolean | true | Show number on active dot |
| className | string | "" | Additional CSS classes |
================================================================================
COMMON CUSTOMIZATION REQUESTS
================================================================================
USER ASKS: "Make dots bigger"
→ Increase dotSize: <DizzyDots dotSize={20} />
USER ASKS: "Change the inactive color"
→ Set baseColor: <DizzyDots baseColor="#666" />
USER ASKS: "Make it smaller/larger"
→ Adjust size AND radius proportionally:
<DizzyDots size={200} radius={80} /> // smaller
<DizzyDots size={500} radius={220} /> // larger
USER ASKS: "Fewer/more dots"
→ Change dotCount: <DizzyDots dotCount={30} /> // 30 dots
<DizzyDots dotCount={120} /> // 120 dots
USER ASKS: "Remove the number"
→ Set showIndex={false}: <DizzyDots showIndex={false} />
USER ASKS: "Different colors on hover"
→ Modify the hsl() in pointColor calculation (line ~108)
================================================================================
HOW IT WORKS (for AI understanding)
================================================================================
1. LAYOUT: Dots arranged in circle using CSS transforms (rotateZ + skewX)
2. INTERACTION: onMouseMove calculates angle from center to cursor
3. HIGHLIGHTING: Nearest dot + adjacent dots (±1, ±2) light up with falloff
4. COLORS: HSL color based on dot position creates rainbow effect
Key constants (top of file):
- SKEW_ANGLE: Controls the orbital plane tilt (83°)
- MICRO_ROTATION: Fine alignment adjustment (2°)
- GLOW_INTENSITY/SPREAD: Control the glow effect
================================================================================
SOURCE CODE
================================================================================
*/
"use client";
import React, { useState, useRef, useCallback, useMemo } from "react";
export interface DizzyDotsProps {
size?: number;
dotCount?: number;
baseColor?: string;
dotSize?: number;
radius?: number;
showIndex?: boolean;
className?: string;
}
const SKEW_ANGLE = 83;
const MICRO_ROTATION = 2;
const GLOW_INTENSITY = 12;
const GLOW_SPREAD = 6;
export default function DizzyDots({
size = 320,
dotCount = 60,
baseColor = "#444",
dotSize = 12,
radius = 200,
showIndex = true,
className = "",
}: DizzyDotsProps) {
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const degreesPerPoint = useMemo(() => 360 / dotCount, [dotCount]);
const pointIndices = useMemo(
() => Array.from({ length: dotCount }, (_, idx) => idx),
[dotCount]
);
const onPointerMove = useCallback(
(evt: React.MouseEvent<HTMLDivElement>) => {
const container = containerRef.current;
if (!container) return;
const bounds = container.getBoundingClientRect();
const relX = evt.clientX - bounds.left - bounds.width / 2;
const relY = evt.clientY - bounds.top - bounds.height / 2;
let polarAngle = Math.atan2(relX, -relY) * (180 / Math.PI);
if (polarAngle < 0) polarAngle += 360;
setFocusedIndex(Math.round(polarAngle / degreesPerPoint) % dotCount);
},
[degreesPerPoint, dotCount]
);
const onPointerExit = useCallback(() => setFocusedIndex(null), []);
const getPointVisuals = useCallback(
(idx: number) => {
if (focusedIndex === null) return { lit: false, intensity: 0 };
const rawDist = Math.abs(idx - focusedIndex);
const circularDist = Math.min(rawDist, dotCount - rawDist);
if (circularDist === 0) return { lit: true, intensity: 1 };
if (circularDist === 1) return { lit: true, intensity: 0.55 };
if (circularDist === 2) return { lit: true, intensity: 0.25 };
return { lit: false, intensity: 0 };
},
[focusedIndex, dotCount]
);
return (
<div
ref={containerRef}
className={`relative ${className}`}
style={{ width: size, height: size }}
onMouseMove={onPointerMove}
onMouseLeave={onPointerExit}
>
<div className="absolute" style={{ left: "50%", top: "50%" }}>
{pointIndices.map((idx) => {
const { lit, intensity } = getPointVisuals(idx);
const hueAngle = idx * degreesPerPoint;
const pointColor = lit ? `hsl(${hueAngle}, 95%, ${52 + intensity * 8}%)` : baseColor;
const pointScale = lit ? 1 + intensity * 0.9 : 1;
return (
<div
key={idx}
className="absolute origin-top-left"
style={{
width: `${radius}px`,
height: `${size * 0.1}px`,
transformStyle: "preserve-3d",
transformOrigin: "top left",
transform: `rotateZ(${idx * degreesPerPoint}deg) skewX(${SKEW_ANGLE}deg)`,
}}
>
<span
className="absolute rounded-full transition-all duration-100 pointer-events-none"
style={{
left: `-${dotSize / 2}px`,
top: `-${dotSize / 2}px`,
width: `${dotSize}px`,
height: `${dotSize}px`,
backgroundColor: pointColor,
transform: `skewX(-${SKEW_ANGLE}deg) rotateZ(${MICRO_ROTATION}deg) translateX(${radius}px) scale(${pointScale})`,
boxShadow: lit
? `0 0 ${GLOW_INTENSITY * intensity}px ${GLOW_SPREAD * intensity}px hsla(${hueAngle}, 95%, 55%, ${intensity * 0.45})`
: "none",
}}
/>
{showIndex && lit && intensity === 1 && (
<span
className="absolute text-white text-4xl font-bold pointer-events-none select-none"
style={{
top: 0, left: 0,
transformOrigin: "top left",
transform: `skewX(-${SKEW_ANGLE}deg) rotateZ(${-idx * degreesPerPoint}deg) translate(-50%, -50%)`,
}}
>
{idx}
</span>
)}
</div>
);
})}
</div>
</div>
);
}
export { DizzyDots };import DizzyDots from "@/components/DizzyDots";
// Basic usage - 60 dots like a clock
function ClockDots() {
return (
<div className="bg-black p-8">
<DizzyDots />
</div>
);
}
// Larger with more dots
function BigDots() {
return (
<div className="bg-black p-8">
<DizzyDots size={500} radius={220} dotCount={90} dotSize={14} />
</div>
);
}
// Compact version
function SmallDots() {
return (
<div className="bg-black p-4">
<DizzyDots size={200} radius={80} dotSize={8} dotCount={40} />
</div>
);
}
// Custom color, no index
function StealthDots() {
return (
<div className="bg-gray-900 p-8">
<DizzyDots baseColor="#222" showIndex={false} />
</div>
);
}
// Hero background
function HeroSection() {
return (
<div className="relative h-screen bg-black">
<div className="absolute inset-0 flex items-center justify-center">
<DizzyDots size={600} radius={280} dotCount={90} />
</div>
<div className="relative z-10 flex items-center justify-center h-full">
<h1 className="text-white text-5xl font-bold">Your Title</h1>
</div>
</div>
);
}