---
title: Stardust Button
description: A premium, highly interactive button component featuring an integrated canvas stardust starfield background. The stardust particles drift ambiently, gravitate and swirl around the cursor on hover, and emit a shimmering burst animation on button click. Includes full light and dark mode adaptivity.
---
## Installation
CLI
Manual
```bash
npx lightswind@latest add stardust-button
```
Copy and paste the following code into your project.
```tsx
"use client";
import React, { useRef, useState, useEffect } from "react";
export interface StardustButtonProps
extends React.ButtonHTMLAttributes {
children: React.ReactNode;
variant?: "cosmic" | "aurora" | "nebula" | "glass";
size?: "sm" | "md" | "lg" | "xl";
className?: string;
particleCount?: number;
particleSpeed?: number;
theme?: "light" | "dark" | "system";
}
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
size: number;
color: string;
alpha: number;
decay: number;
angle?: number;
speed?: number;
orbitRadius?: number;
}
export function StardustButton({
children,
variant = "cosmic",
size = "md",
className = "",
particleCount = 45,
particleSpeed = 1.0,
theme = "system",
...props
}: StardustButtonProps) {
const buttonRef = useRef(null);
const canvasRef = useRef(null);
const [isHovered, setIsHovered] = useState(false);
const [isPressed, setIsPressed] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(true);
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
// Theme tracking
useEffect(() => {
const checkTheme = () => {
if (theme === "system") {
setIsDarkMode(document.documentElement.classList.contains("dark"));
} else {
setIsDarkMode(theme === "dark");
}
};
checkTheme();
if (theme === "system") {
const observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
}
}, [theme]);
// Canvas particle logic
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animationFrameId: number;
let particles: Particle[] = [];
let width = (canvas.width = canvas.offsetWidth);
let height = (canvas.height = canvas.offsetHeight);
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
width = canvas.width = entry.contentRect.width;
height = canvas.height = entry.contentRect.height;
}
});
if (buttonRef.current) {
resizeObserver.observe(buttonRef.current);
}
const particleColors = {
cosmic: isDarkMode
? ["#6366f1", "#a855f7", "#ec4899", "#fbbf24"]
: ["#4f46e5", "#7c3aed", "#db2777", "#d97706"],
aurora: isDarkMode
? ["#10b981", "#06b6d4", "#6366f1", "#a855f7"]
: ["#059669", "#0891b2", "#2563eb", "#7c3aed"],
nebula: isDarkMode
? ["#ec4899", "#f43f5e", "#d946ef", "#8b5cf6"]
: ["#db2777", "#e11d48", "#c084fc", "#6d28d9"],
glass: isDarkMode
? ["#ffffff", "#e4e4e7", "#a1a1aa", "#d4d4d8"]
: ["#09090b", "#27272a", "#52525b", "#71717a"],
};
const colors = particleColors[variant] || particleColors.cosmic;
const createParticle = (x: number, y: number, isBurst = false): Particle => {
const sizeVal = Math.random() * 1.5 + 0.5;
const angle = Math.random() * Math.PI * 2;
const speed = isBurst
? Math.random() * 3 + 1
: Math.random() * 0.6 + 0.2;
return {
x,
y,
vx: Math.cos(angle) * speed * particleSpeed,
vy: Math.sin(angle) * speed * particleSpeed,
size: sizeVal,
color: colors[Math.floor(Math.random() * colors.length)],
alpha: isBurst ? 1.0 : Math.random() * 0.6 + 0.4,
decay: isBurst ? Math.random() * 0.03 + 0.02 : Math.random() * 0.008 + 0.005,
angle: angle,
orbitRadius: Math.random() * 40 + 10,
speed: speed,
};
};
// Initialize ambient particles
for (let i = 0; i < (isHovered ? particleCount : particleCount / 3); i++) {
particles.push(
createParticle(Math.random() * width, Math.random() * height)
);
}
const handleBurst = (e: Event) => {
const { x, y } = (e as CustomEvent).detail;
for (let i = 0; i < 20; i++) {
particles.push(createParticle(x, y, true));
}
};
canvas.addEventListener("particle-burst", handleBurst);
const animate = () => {
ctx.clearRect(0, 0, width, height);
// Add ambient particles if count is below threshold
const targetCount = isHovered ? particleCount : particleCount / 3;
if (particles.length < targetCount && Math.random() < 0.3) {
particles.push(
createParticle(
isHovered ? mousePos.x + (Math.random() * 20 - 10) : Math.random() * width,
isHovered ? mousePos.y + (Math.random() * 20 - 10) : Math.random() * height
)
);
}
particles.forEach((p, idx) => {
// Apply physics
if (isHovered) {
// Attract towards cursor
const dx = mousePos.x - p.x;
const dy = mousePos.y - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 80) {
// Apply slight orbit/swirl motion around cursor
const force = (80 - dist) / 80;
p.vx += (dx / dist) * force * 0.08;
p.vy += (dy / dist) * force * 0.08;
// Swirl component
p.vx += -(dy / dist) * force * 0.15;
p.vy += (dx / dist) * force * 0.15;
}
}
p.x += p.vx;
p.y += p.vy;
p.alpha -= p.decay;
// Bounce/Wrap borders
if (p.x < 0 || p.x > width) p.vx *= -1;
if (p.y < 0 || p.y > height) p.vy *= -1;
// Draw particle
ctx.save();
ctx.globalAlpha = Math.max(0, p.alpha);
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = p.color;
// Soft glow for sparkling effect
if (p.size > 1.2) {
ctx.shadowBlur = 4;
ctx.shadowColor = p.color;
}
ctx.fill();
ctx.restore();
// Filter out dead particles
if (p.alpha <= 0) {
particles.splice(idx, 1);
}
});
animationFrameId = requestAnimationFrame(animate);
};
animate();
return () => {
cancelAnimationFrame(animationFrameId);
resizeObserver.disconnect();
canvas.removeEventListener("particle-burst", handleBurst);
};
}, [isHovered, mousePos, variant, particleCount, particleSpeed, isDarkMode]);
const handleMouseMove = (e: React.MouseEvent) => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
setMousePos({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
};
const handleMouseDown = (e: React.MouseEvent) => {
setIsPressed(true);
// Trigger burst on click
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const customEvent = new CustomEvent("particle-burst", {
detail: { x: clickX, y: clickY },
});
canvas.dispatchEvent(customEvent);
};
// Size configurations
const sizeClasses = {
sm: "text-xs px-4 py-2 rounded-lg",
md: "text-sm px-6 py-3 rounded-xl",
lg: "text-base px-8 py-4 rounded-2xl",
xl: "text-lg px-10 py-5 rounded-[20px]",
};
const getStyles = () => {
if (isDarkMode) {
switch (variant) {
case "aurora":
return {
bg: "bg-emerald-950/20 backdrop-blur-md",
border: "border-emerald-500/20 hover:border-emerald-400/40",
textColor: "text-emerald-300 hover:text-white",
glow: "shadow-[0_0_20px_rgba(16,185,129,0.15)]",
};
case "nebula":
return {
bg: "bg-pink-955/15 backdrop-blur-md",
border: "border-pink-500/20 hover:border-pink-400/40",
textColor: "text-pink-300 hover:text-white",
glow: "shadow-[0_0_20px_rgba(236,72,153,0.15)]",
};
case "glass":
return {
bg: "bg-white/5 backdrop-blur-md",
border: "border-white/10 hover:border-white/20",
textColor: "text-zinc-200 hover:text-white",
glow: "shadow-none",
};
case "cosmic":
default:
return {
bg: "bg-indigo-950/25 backdrop-blur-md",
border: "border-indigo-500/25 hover:border-indigo-400/45",
textColor: "text-indigo-300 hover:text-white",
glow: "shadow-[0_0_20px_rgba(99,102,241,0.2)]",
};
}
} else {
// Light Mode styles
switch (variant) {
case "aurora":
return {
bg: "bg-emerald-50/70 backdrop-blur-md",
border: "border-emerald-200 hover:border-emerald-300/60",
textColor: "text-emerald-700 hover:text-emerald-950",
glow: "shadow-[0_0_15px_rgba(16,185,129,0.1)]",
};
case "nebula":
return {
bg: "bg-pink-50/70 backdrop-blur-md",
border: "border-pink-200 hover:border-pink-300/60",
textColor: "text-pink-700 hover:text-pink-950",
glow: "shadow-[0_0_15px_rgba(236,72,153,0.1)]",
};
case "glass":
return {
bg: "bg-black/5 backdrop-blur-md",
border: "border-black/5 hover:border-black/15",
textColor: "text-zinc-800 hover:text-black",
glow: "shadow-none",
};
case "cosmic":
default:
return {
bg: "bg-indigo-50/70 backdrop-blur-md",
border: "border-indigo-200/50 hover:border-indigo-300/80",
textColor: "text-indigo-700 hover:text-indigo-950",
glow: "shadow-[0_0_15px_rgba(99,102,241,0.1)]",
};
}
}
};
const style = getStyles();
return (
);
}
export default StardustButton;
```
## Usage
```tsx
import StardustButton from "@/components/lightswind/stardust-button"
```
```tsx
import StardustButton from "@/components/lightswind/stardust-button";
import { Star } from "lucide-react";
export function Demo() {
return (
Sparkle Dust
);
}
```