Satisfying click-anywhere bursts with streaks, blobs, and expanding waves
Power
Copy URL or Code
Paste to your AI coding assistant and customize:
Done. Your AI handles the rest.
Fully customizable. 3 power levels, custom color themes, click-anywhere interaction. Pure React + Canvas, no external dependencies.
/*
================================================================================
AI COMPONENT: ClickBoom
================================================================================
SETUP:
1. Create file: components/ClickBoom.tsx
2. Copy this entire code block into that file
3. Import: import ClickBoom from "@/components/ClickBoom"
4. No external dependencies required - pure React + Canvas
================================================================================
QUICK CUSTOMIZATION (via props)
================================================================================
| Prop | Type | Default |
|---------------|-------------------------------------|-------------|
| power | "gentle" | "normal" | "explosive" | "normal" |
| showHint | boolean | true |
| colors.coral | string (hex) | "#FF6B7A" |
| colors.peach | string (hex) | "#FFD4A8" |
| colors.wine | string (hex) | "#B8132D" |
| colors.slate | string (hex/background) | "#2D3E45" |
| forceTrigger | boolean (for external control) | false |
================================================================================
POWER LEVELS
================================================================================
| Level | Particles | Waves | Speed | Description |
|-----------|-----------|-------|----------|--------------------------|
| gentle | 14 | 1 | slower | Subtle, elegant burst |
| normal | 24 | 2 | standard | Balanced, satisfying |
| explosive | 38 | 3 | faster | Dramatic, energetic |
================================================================================
PARTICLE TYPES
================================================================================
- Blobs: Large circular shapes in background
- Streaks: Radiating line strokes from center
- Halos: Hollow circle outlines
- Chips: Solid square confetti
================================================================================
COMMON MODIFICATIONS
================================================================================
1. BASIC CLICK AREA:
<div className="w-full h-[400px]">
<ClickBoom />
</div>
2. EXPLOSIVE POWER:
<ClickBoom power="explosive" />
3. CUSTOM COLORS:
<ClickBoom colors={{ coral: "#00D9FF", peach: "#FF00FF", wine: "#FFFF00", slate: "#1a1a2e" }} />
4. NO HINT TEXT:
<ClickBoom showHint={false} />
5. FULL PAGE BACKGROUND:
<div className="fixed inset-0">
<ClickBoom power="explosive" showHint={false} />
</div>
================================================================================
SOURCE CODE
================================================================================
*/
"use client";
import React, { useRef, useEffect, useCallback, useState } from "react";
export type BurstPower = "gentle" | "normal" | "explosive";
export interface ClickBoomColors {
coral?: string;
peach?: string;
wine?: string;
slate?: string;
}
export interface ClickBoomProps {
colors?: ClickBoomColors;
power?: BurstPower;
showHint?: boolean;
className?: string;
forceTrigger?: boolean;
}
const defaultColors: Required<ClickBoomColors> = {
coral: "#FF6B7A",
peach: "#FFD4A8",
wine: "#B8132D",
slate: "#2D3E45",
};
const LAUNCH_SPEED = 4.5;
const SPEED_VARIANCE = 5.5;
const PULL_DOWN = 0.065;
const AIR_DRAG = 0.965;
const PARTICLE_DURATION = 48;
interface Fleck {
x: number;
y: number;
dx: number;
dy: number;
radius: number;
hue: string;
opacity: number;
spin: number;
spinRate: number;
age: number;
lifespan: number;
kind: "blob" | "streak" | "halo" | "chip";
}
interface Wave {
x: number;
y: number;
size: number;
targetSize: number;
opacity: number;
width: number;
hue: string;
}
interface BurstData {
flecks: Fleck[];
waves: Wave[];
}
export default function ClickBoom({
colors: customColors,
power = "normal",
showHint = true,
className = "",
forceTrigger = false,
}: ClickBoomProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const frameRef = useRef<number | undefined>(undefined);
const dataRef = useRef<BurstData>({ flecks: [], waves: [] });
const [hasInteracted, setHasInteracted] = useState(false);
const colors = { ...defaultColors, ...customColors };
const palette = [colors.coral, colors.peach, "white", colors.wine];
const powerSettings = {
gentle: { count: 14, waveCount: 1, speedMod: 0.65 },
normal: { count: 24, waveCount: 2, speedMod: 1 },
explosive: { count: 38, waveCount: 3, speedMod: 1.35 },
};
const settings = powerSettings[power];
const spawnBurst = useCallback((cx: number, cy: number) => {
const flecks: Fleck[] = [];
const waves: Wave[] = [];
for (let w = 0; w < settings.waveCount; w++) {
waves.push({
x: cx, y: cy,
size: 12 + w * 18,
targetSize: 135 + w * 45,
opacity: 0.55 - w * 0.12,
width: 14 - w * 4,
hue: w === 0 ? colors.coral : w === 1 ? "white" : colors.slate,
});
}
const blobCount = Math.floor(settings.count * 0.28);
for (let i = 0; i < blobCount; i++) {
const theta = (i / blobCount) * Math.PI * 2 + Math.random() * 0.4;
const speed = (LAUNCH_SPEED * 0.45 + Math.random() * SPEED_VARIANCE * 0.35) * settings.speedMod;
flecks.push({
x: cx, y: cy,
dx: Math.cos(theta) * speed,
dy: Math.sin(theta) * speed,
radius: 18 + Math.random() * 22,
hue: palette[i % 3],
opacity: 1,
spin: 0,
spinRate: 0,
age: 0,
lifespan: PARTICLE_DURATION * 0.75,
kind: "blob",
});
}
const streakCount = Math.floor(settings.count * 0.36);
for (let i = 0; i < streakCount; i++) {
const theta = (i / streakCount) * Math.PI * 2;
const speed = (LAUNCH_SPEED + Math.random() * SPEED_VARIANCE) * settings.speedMod;
flecks.push({
x: cx, y: cy,
dx: Math.cos(theta) * speed,
dy: Math.sin(theta) * speed,
radius: 22 + Math.random() * 12,
hue: Math.random() > 0.5 ? "white" : colors.wine,
opacity: 1,
spin: theta,
spinRate: 0,
age: 0,
lifespan: PARTICLE_DURATION,
kind: "streak",
});
}
const shapeCount = Math.floor(settings.count * 0.36);
for (let i = 0; i < shapeCount; i++) {
const theta = Math.random() * Math.PI * 2;
const speed = (LAUNCH_SPEED * 0.75 + Math.random() * SPEED_VARIANCE * 0.85) * settings.speedMod;
flecks.push({
x: cx, y: cy,
dx: Math.cos(theta) * speed,
dy: Math.sin(theta) * speed,
radius: 12 + Math.random() * 18,
hue: palette[i % palette.length],
opacity: 1,
spin: Math.random() * Math.PI,
spinRate: (Math.random() - 0.5) * 0.12,
age: 0,
lifespan: PARTICLE_DURATION * 1.05,
kind: Math.random() > 0.5 ? "halo" : "chip",
});
}
dataRef.current = {
flecks: [...dataRef.current.flecks, ...flecks],
waves: [...dataRef.current.waves, ...waves],
};
}, [colors, palette, settings]);
const render = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
const w = canvas.width / dpr;
const h = canvas.height / dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
const { flecks, waves } = dataRef.current;
let stillActive = false;
const liveWaves: Wave[] = [];
waves.forEach((wave) => {
const progress = (wave.size - 12) / (wave.targetSize - 12);
if (progress < 1) {
stillActive = true;
wave.size += 4.5;
wave.opacity = (1 - progress) * 0.55;
wave.width = Math.max(1, wave.width * 0.94);
ctx.beginPath();
ctx.arc(wave.x, wave.y, wave.size, 0, Math.PI * 2);
ctx.strokeStyle = wave.hue;
ctx.globalAlpha = wave.opacity;
ctx.lineWidth = wave.width;
ctx.stroke();
ctx.globalAlpha = 1;
liveWaves.push(wave);
}
});
const liveFlecks: Fleck[] = [];
flecks.forEach((f) => {
if (f.age < f.lifespan) {
stillActive = true;
f.age++;
f.x += f.dx;
f.y += f.dy;
f.dy += PULL_DOWN;
f.dx *= AIR_DRAG;
f.dy *= AIR_DRAG;
f.spin += f.spinRate;
f.opacity = 1 - f.age / f.lifespan;
ctx.save();
ctx.translate(f.x, f.y);
ctx.rotate(f.spin);
ctx.globalAlpha = f.opacity * 0.88;
ctx.fillStyle = f.hue;
ctx.strokeStyle = f.hue;
if (f.kind === "blob") {
ctx.beginPath();
ctx.arc(0, 0, f.radius / 2, 0, Math.PI * 2);
ctx.fill();
} else if (f.kind === "streak") {
ctx.lineWidth = 3.5;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(0, -f.radius / 2);
ctx.lineTo(0, f.radius / 2);
ctx.stroke();
} else if (f.kind === "halo") {
ctx.lineWidth = 2.8;
ctx.beginPath();
ctx.arc(0, 0, f.radius / 2, 0, Math.PI * 2);
ctx.stroke();
} else if (f.kind === "chip") {
ctx.fillRect(-f.radius / 2, -f.radius / 2, f.radius, f.radius);
}
ctx.restore();
liveFlecks.push(f);
}
});
dataRef.current = { flecks: liveFlecks, waves: liveWaves };
if (stillActive) {
frameRef.current = requestAnimationFrame(render);
}
}, []);
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setHasInteracted(true);
spawnBurst(x, y);
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
frameRef.current = requestAnimationFrame(render);
}, [spawnBurst, render]);
const prevTrigger = useRef(forceTrigger);
useEffect(() => {
if (forceTrigger && !prevTrigger.current) {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
const dpr = window.devicePixelRatio || 1;
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
}
dataRef.current = { flecks: [], waves: [] };
const rect = container.getBoundingClientRect();
setHasInteracted(true);
spawnBurst(rect.width / 2, rect.height / 2);
frameRef.current = requestAnimationFrame(render);
}
prevTrigger.current = forceTrigger;
}, [forceTrigger, spawnBurst, render]);
useEffect(() => {
const resizeCanvas = () => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const dpr = window.devicePixelRatio || 1;
const rect = container.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
};
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
return () => {
window.removeEventListener("resize", resizeCanvas);
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
};
}, []);
return (
<div
ref={containerRef}
onClick={handleClick}
className={`relative w-full h-full cursor-pointer select-none overflow-hidden ${className}`}
style={{ backgroundColor: colors.slate, minHeight: 200 }}
>
<canvas ref={canvasRef} className="absolute inset-0 pointer-events-none" />
{showHint && !hasInteracted && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div
className="text-white/60 text-lg font-medium tracking-wide animate-pulse"
style={{ fontFamily: "system-ui, sans-serif" }}
>
click anywhere
</div>
</div>
)}
</div>
);
}
export { ClickBoom };import ClickBoom from "@/components/ClickBoom";
// Power levels: "gentle", "normal", "explosive"
// Basic click area with hint
export default function BasicClickBoom() {
return (
<div className="w-full h-[400px]">
<ClickBoom />
</div>
);
}
// Explosive power, no hint
export function ExplosiveBursts() {
return (
<div className="w-full h-[400px]">
<ClickBoom power="explosive" showHint={false} />
</div>
);
}
// Custom colors
export function CustomColors() {
return (
<div className="w-full h-[400px]">
<ClickBoom
colors={{
coral: "#00D9FF",
peach: "#FF00FF",
wine: "#FFFF00",
slate: "#1a1a2e",
}}
/>
</div>
);
}
// Full page background effect
export function FullPageBackground() {
return (
<div className="fixed inset-0">
<ClickBoom power="explosive" showHint={false} />
<div className="relative z-10 flex items-center justify-center h-full">
<h1 className="text-6xl font-bold text-white">Click Anywhere!</h1>
</div>
</div>
);
}
// Gentle, elegant burst for subtle UI
export function SubtleFeedback() {
return (
<div className="w-full h-[300px] rounded-xl overflow-hidden">
<ClickBoom power="gentle" showHint />
</div>
);
}