"use client";
import * as React from "react";
import { createPortal } from "react-dom";
import {
Bodies,
Body,
Composite,
Engine,
World,
} from "matter-js";
import { cn } from "@/lib/utils";
/** Mostly charcoal and ash grey, with a few embers. */
const ASH_COLORS = [
"#171717",
"#262626",
"#404040",
"#525252",
"#737373",
"#a3a3a3",
"#b91c1c",
"#f97316",
];
/** How long the ember glow stays on the button after a burst. */
const FIRE_MS = 480;
type ParticleKind = "circle" | "square" | "shard";
type ParticleMeta = {
color: string;
kind: ParticleKind;
size: number;
born: number;
life: number;
glow: boolean;
};
type AshSim = {
engine: Matter.Engine;
buttonBody: Matter.Body;
leftLip: Matter.Body;
rightLip: Matter.Body;
meta: Map<number, ParticleMeta>;
raf: number;
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
startedAt: number;
lastW: number;
lastH: number;
};
export interface AshBurstButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Label used when no children are provided. */
label?: React.ReactNode;
/** Ash particles per burst. */
particleCount?: number;
/** Burst spread in degrees. */
spread?: number;
/** Extra launch velocity (mapped into Matter velocity). */
startVelocity?: number;
/** Custom ash / ember colors. */
colors?: string[];
/** Show the leading ember icon. */
icon?: boolean;
/** Fired after each ash burst. */
onDestroy?: () => void;
}
/** Warm reds and oranges get a soft glow; greys stay flat. */
function isEmber(color: string) {
const hex = color.replace("#", "");
if (hex.length < 6) return false;
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return r > 150 && r > g + 40 && r > b + 60;
}
function pick<T>(items: T[]): T {
return items[Math.floor(Math.random() * items.length)]!;
}
function drawShard(
ctx: CanvasRenderingContext2D,
size: number,
color: string,
) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(0, -size);
ctx.lineTo(size * 0.55, -size * 0.15);
ctx.lineTo(size * 0.35, size);
ctx.lineTo(-size * 0.45, size * 0.55);
ctx.lineTo(-size * 0.2, -size * 0.35);
ctx.closePath();
ctx.fill();
}
function sizeCanvas(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = window.innerWidth;
const h = window.innerHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = `${w}px`;
canvas.style.height = `${h}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function syncColliders(sim: AshSim, button: HTMLElement) {
const rect = button.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const w = Math.max(8, rect.width);
const h = Math.max(8, rect.height);
if (sim.lastW > 0 && sim.lastH > 0) {
const sx = w / sim.lastW;
const sy = h / sim.lastH;
if (Math.abs(sx - 1) > 0.005 || Math.abs(sy - 1) > 0.005) {
Body.scale(sim.buttonBody, sx, sy);
}
}
Body.setPosition(sim.buttonBody, { x: cx, y: cy });
const lipW = Math.max(10, w * 0.1);
const lipH = 7;
Body.setPosition(sim.leftLip, {
x: rect.left + lipW / 2,
y: rect.top - lipH / 2 + 1,
});
Body.setPosition(sim.rightLip, {
x: rect.right - lipW / 2,
y: rect.top - lipH / 2 + 1,
});
sim.lastW = w;
sim.lastH = h;
}
function createAshSimulation(
canvas: HTMLCanvasElement,
button: HTMLElement,
options: {
particleCount: number;
spread: number;
startVelocity: number;
colors: string[];
},
): AshSim {
const ctx = canvas.getContext("2d")!;
sizeCanvas(canvas, ctx);
const engine = Engine.create({
gravity: { x: 0, y: 1.4 },
});
const rect = button.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const w = Math.max(8, rect.width);
const h = Math.max(8, rect.height);
const buttonBody = Bodies.rectangle(cx, cy, w, h, {
isStatic: true,
friction: 1.25,
frictionStatic: 1.4,
restitution: 0.18,
chamfer: { radius: Math.min(8, h / 2) },
label: "ash-button",
});
const lipW = Math.max(10, w * 0.1);
const lipH = 7;
const leftLip = Bodies.rectangle(
rect.left + lipW / 2,
rect.top - lipH / 2 + 1,
lipW,
lipH,
{
isStatic: true,
friction: 1.4,
restitution: 0.04,
label: "ash-lip",
},
);
const rightLip = Bodies.rectangle(
rect.right - lipW / 2,
rect.top - lipH / 2 + 1,
lipW,
lipH,
{
isStatic: true,
friction: 1.4,
restitution: 0.04,
label: "ash-lip",
},
);
World.add(engine.world, [buttonBody, leftLip, rightLip]);
const meta = new Map<number, ParticleMeta>();
const now = performance.now();
const count = Math.max(12, options.particleCount);
for (let i = 0; i < count; i++) {
const kindRoll = Math.random();
const kind: ParticleKind =
kindRoll > 0.75 ? "shard" : kindRoll > 0.4 ? "square" : "circle";
const size =
kind === "shard" ? 2.5 + Math.random() * 3 : 1.6 + Math.random() * 2.6;
const halfSpread = (options.spread * Math.PI) / 180 / 2;
const angle = -Math.PI / 2 + (Math.random() * 2 - 1) * halfSpread;
// Balanced kick: readable burst without flying off too hard.
const speed =
options.startVelocity * (0.45 + Math.random() * 0.55) * 0.7;
// Mostly explode from center; a few rain down later to settle on top.
const rain = Math.random() > 0.72;
const spawnX = rain
? rect.left + Math.random() * w
: cx + (Math.random() - 0.5) * w * 0.4;
const spawnY = rain
? rect.top - 8 - Math.random() * 28
: cy + (Math.random() - 0.5) * h * 0.3;
const body =
kind === "circle"
? Bodies.circle(spawnX, spawnY, size * 0.85, {
restitution: 0.35 + Math.random() * 0.3,
friction: 0.65 + Math.random() * 0.4,
frictionAir: 0.008 + Math.random() * 0.012,
density: 0.0012,
label: "ash-particle",
})
: Bodies.rectangle(
spawnX,
spawnY,
size * (kind === "shard" ? 1.4 : 1.6),
size * (kind === "shard" ? 2.2 : 1.6),
{
restitution: 0.3 + Math.random() * 0.28,
friction: 0.7 + Math.random() * 0.4,
frictionAir: 0.009 + Math.random() * 0.012,
density: 0.0013,
angle: Math.random() * Math.PI,
label: "ash-particle",
},
);
if (rain) {
Body.setVelocity(body, {
x: (Math.random() - 0.5) * 3.5,
y: 1 + Math.random() * 3,
});
} else {
Body.setVelocity(body, {
x: Math.cos(angle) * speed + (Math.random() - 0.5) * 4,
y: Math.sin(angle) * speed - (2 + Math.random() * 5),
});
}
Body.setAngularVelocity(body, (Math.random() - 0.5) * 0.55);
const color = pick(options.colors);
meta.set(body.id, {
color,
glow: isEmber(color),
kind,
size,
born: now,
life: 3200 + Math.random() * 2200,
});
World.add(engine.world, body);
}
return {
engine,
buttonBody,
leftLip,
rightLip,
meta,
raf: 0,
canvas,
ctx,
startedAt: now,
lastW: w,
lastH: h,
};
}
function paintSim(sim: AshSim, button: HTMLElement) {
const { ctx, canvas, engine, meta } = sim;
const w = canvas.clientWidth;
const h = canvas.clientHeight;
syncColliders(sim, button);
Engine.update(engine, 1000 / 60);
ctx.clearRect(0, 0, w, h);
const now = performance.now();
const toRemove: Matter.Body[] = [];
for (const body of Composite.allBodies(engine.world)) {
if (body.label !== "ash-particle") continue;
const info = meta.get(body.id);
if (!info) continue;
const age = now - info.born;
const fade = Math.max(0, 1 - age / info.life);
if (
fade <= 0 ||
body.position.y > h + 48 ||
body.position.x < -48 ||
body.position.x > w + 48
) {
toRemove.push(body);
continue;
}
ctx.save();
ctx.translate(body.position.x, body.position.y);
ctx.rotate(body.angle);
ctx.globalAlpha = 0.4 + fade * 0.6;
if (info.glow) {
ctx.globalAlpha = fade * 0.22;
ctx.fillStyle = info.color;
ctx.beginPath();
ctx.arc(0, 0, info.size * 2, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 0.4 + fade * 0.6;
}
if (info.kind === "shard") {
drawShard(ctx, info.size, info.color);
} else if (info.kind === "square") {
ctx.fillStyle = info.color;
ctx.fillRect(-info.size, -info.size, info.size * 2, info.size * 2);
} else {
ctx.fillStyle = info.color;
ctx.beginPath();
ctx.arc(0, 0, info.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
for (const body of toRemove) {
meta.delete(body.id);
World.remove(engine.world, body);
}
return meta.size > 0 && now - sim.startedAt < 6500;
}
export const AshBurstButton = React.forwardRef<
HTMLButtonElement,
AshBurstButtonProps
>(
(
{
children,
label = "Delete",
particleCount = 80,
spread = 110,
startVelocity = 42,
colors = ASH_COLORS,
icon = true,
onDestroy,
onClick,
className,
disabled,
type = "button",
...props
},
ref,
) => {
const buttonRef = React.useRef<HTMLButtonElement | null>(null);
const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
const simRef = React.useRef<AshSim | null>(null);
const fireTimerRef = React.useRef<number | undefined>(undefined);
const [simActive, setSimActive] = React.useState(false);
const [firing, setFiring] = React.useState(false);
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
const setButtonRef = (node: HTMLButtonElement | null) => {
buttonRef.current = node;
if (typeof ref === "function") ref(node);
else if (ref) ref.current = node;
};
const clearSim = (sim: AshSim) => {
cancelAnimationFrame(sim.raf);
World.clear(sim.engine.world, false);
Engine.clear(sim.engine);
sim.ctx.clearRect(0, 0, sim.canvas.clientWidth, sim.canvas.clientHeight);
};
const stopSim = React.useCallback(() => {
if (simRef.current) clearSim(simRef.current);
simRef.current = null;
setSimActive(false);
}, []);
React.useEffect(
() => () => {
stopSim();
window.clearTimeout(fireTimerRef.current);
},
[stopSim],
);
const startPhysicsBurst = () => {
const button = buttonRef.current;
const canvas = canvasRef.current;
if (!button || !canvas) return;
if (simRef.current) clearSim(simRef.current);
setSimActive(true);
const sim = createAshSimulation(canvas, button, {
particleCount,
spread,
startVelocity,
colors,
});
simRef.current = sim;
const tick = () => {
const current = simRef.current;
const btn = buttonRef.current;
if (!current || !btn) return;
if (paintSim(current, btn)) {
current.raf = requestAnimationFrame(tick);
} else {
stopSim();
}
};
sim.raf = requestAnimationFrame(tick);
};
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event);
if (disabled || event.defaultPrevented) return;
const reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
if (!reduceMotion) startPhysicsBurst();
setFiring(true);
window.clearTimeout(fireTimerRef.current);
fireTimerRef.current = window.setTimeout(() => setFiring(false), FIRE_MS);
onDestroy?.();
};
const overlay = mounted
? createPortal(
<canvas
ref={canvasRef}
aria-hidden
className="pointer-events-none fixed inset-0 z-[80]"
style={{ opacity: simActive ? 1 : 0 }}
/>,
document.body,
)
: null;
return (
<>
{overlay}
<button
ref={setButtonRef}
type={type}
disabled={disabled}
data-firing={firing || undefined}
onClick={handleClick}
className={cn(
"group/ash inline-flex h-9 cursor-pointer items-center justify-center gap-2 rounded-[10px] px-4 text-sm font-medium text-neutral-50 outline-none select-none",
// Graded dark surface: dark outer hairline, faint inner ring, top highlight, soft drop.
"bg-linear-to-b from-[#353535] to-[#272727] shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.06),inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.25),0_4px_12px_-4px_rgb(0_0_0/0.4)]",
"transition-[scale,filter,box-shadow] duration-300 ease-[cubic-bezier(0.34,1.35,0.64,1)] hover:brightness-110",
// Press eases in fast and settles the shadow; release springs back on the slower base curve.
"active:scale-[0.97] active:brightness-95 active:duration-100 active:ease-out active:shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.05),inset_0_1px_0_rgb(255_255_255/0.08),0_0_1px_rgb(0_0_0/0.2),0_1px_3px_-2px_rgb(0_0_0/0.3)] motion-reduce:active:scale-100",
// A burst leaves a faint ember glow on the face that cools back to the idle surface.
"data-firing:shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(249_115_22/0.18),inset_0_1px_0_rgb(255_255_255/0.14),inset_0_-8px_16px_-8px_rgb(239_68_68/0.35),0_1px_2px_rgb(0_0_0/0.25),0_4px_16px_-4px_rgb(239_68_68/0.35)]",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
icon && "pl-3",
className,
)}
{...props}
>
{icon ? (
<svg
aria-hidden
viewBox="0 0 16 16"
fill="currentColor"
className={cn(
"size-3.5 text-neutral-50/45 transition-[translate,scale,color] duration-300 ease-[cubic-bezier(0.34,1.35,0.64,1)]",
"group-hover/ash:text-red-400/80",
"group-data-firing/ash:-translate-y-px group-data-firing/ash:scale-110 group-data-firing/ash:text-orange-400 motion-reduce:transform-none",
)}
>
<path d="M8.6 1.2c.3 2-.6 3.1-1.6 4.2C5.9 6.6 4.5 8 4.5 10.2 4.5 12.6 6.2 14.5 8 14.5s3.5-1.6 3.5-4.1c0-1.5-.6-2.6-1.3-3.4-.1 1.1-.6 1.8-1.4 2.1.5-2.7-.1-5.8-2.2-7.9Z" />
</svg>
) : null}
<span>{children ?? label}</span>
</button>
</>
);
},
);
AshBurstButton.displayName = "AshBurstButton";
export default AshBurstButton;
AshBurstButton
By jay sharma
Try the interaction, copy the React source files, or download the complete example.
About AshBurstButton
AshBurstButton is an animated React and Tailwind CSS button by jay sharma. Try the interaction, copy the React source, or download the example with creator credit.
- Effects and categories
- Hover effects, Animated
- Using this example
- Copy a TSX file or download the original source and standalone demo. Test keyboard focus, motion and contrast in your own project.
Usage, Tailwind and licensing questions
Read the source files
import-examples/ash-burst-button.tsx
import { AshBurstButton } from "@/components/evil-buttons/ash-burst-button";
export function ButtonDemo() {
return (
<AshBurstButton
colors={["#171717", "#404040", "#737373", "#f97316"]}
particleCount={120}
onDestroy={() => console.log("Burned.")}
>
Incinerate
</AshBurstButton>
);
}
components/evil-buttons/ash-burst-button.tsx
"use client";
import * as React from "react";
import { createPortal } from "react-dom";
import {
Bodies,
Body,
Composite,
Engine,
World,
} from "matter-js";
import { cn } from "@/lib/utils";
/** Mostly charcoal and ash grey, with a few embers. */
const ASH_COLORS = [
"#171717",
"#262626",
"#404040",
"#525252",
"#737373",
"#a3a3a3",
"#b91c1c",
"#f97316",
];
/** How long the ember glow stays on the button after a burst. */
const FIRE_MS = 480;
type ParticleKind = "circle" | "square" | "shard";
type ParticleMeta = {
color: string;
kind: ParticleKind;
size: number;
born: number;
life: number;
glow: boolean;
};
type AshSim = {
engine: Matter.Engine;
buttonBody: Matter.Body;
leftLip: Matter.Body;
rightLip: Matter.Body;
meta: Map<number, ParticleMeta>;
raf: number;
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
startedAt: number;
lastW: number;
lastH: number;
};
export interface AshBurstButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Label used when no children are provided. */
label?: React.ReactNode;
/** Ash particles per burst. */
particleCount?: number;
/** Burst spread in degrees. */
spread?: number;
/** Extra launch velocity (mapped into Matter velocity). */
startVelocity?: number;
/** Custom ash / ember colors. */
colors?: string[];
/** Show the leading ember icon. */
icon?: boolean;
/** Fired after each ash burst. */
onDestroy?: () => void;
}
/** Warm reds and oranges get a soft glow; greys stay flat. */
function isEmber(color: string) {
const hex = color.replace("#", "");
if (hex.length < 6) return false;
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return r > 150 && r > g + 40 && r > b + 60;
}
function pick<T>(items: T[]): T {
return items[Math.floor(Math.random() * items.length)]!;
}
function drawShard(
ctx: CanvasRenderingContext2D,
size: number,
color: string,
) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(0, -size);
ctx.lineTo(size * 0.55, -size * 0.15);
ctx.lineTo(size * 0.35, size);
ctx.lineTo(-size * 0.45, size * 0.55);
ctx.lineTo(-size * 0.2, -size * 0.35);
ctx.closePath();
ctx.fill();
}
function sizeCanvas(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = window.innerWidth;
const h = window.innerHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = `${w}px`;
canvas.style.height = `${h}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function syncColliders(sim: AshSim, button: HTMLElement) {
const rect = button.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const w = Math.max(8, rect.width);
const h = Math.max(8, rect.height);
if (sim.lastW > 0 && sim.lastH > 0) {
const sx = w / sim.lastW;
const sy = h / sim.lastH;
if (Math.abs(sx - 1) > 0.005 || Math.abs(sy - 1) > 0.005) {
Body.scale(sim.buttonBody, sx, sy);
}
}
Body.setPosition(sim.buttonBody, { x: cx, y: cy });
const lipW = Math.max(10, w * 0.1);
const lipH = 7;
Body.setPosition(sim.leftLip, {
x: rect.left + lipW / 2,
y: rect.top - lipH / 2 + 1,
});
Body.setPosition(sim.rightLip, {
x: rect.right - lipW / 2,
y: rect.top - lipH / 2 + 1,
});
sim.lastW = w;
sim.lastH = h;
}
function createAshSimulation(
canvas: HTMLCanvasElement,
button: HTMLElement,
options: {
particleCount: number;
spread: number;
startVelocity: number;
colors: string[];
},
): AshSim {
const ctx = canvas.getContext("2d")!;
sizeCanvas(canvas, ctx);
const engine = Engine.create({
gravity: { x: 0, y: 1.4 },
});
const rect = button.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const w = Math.max(8, rect.width);
const h = Math.max(8, rect.height);
const buttonBody = Bodies.rectangle(cx, cy, w, h, {
isStatic: true,
friction: 1.25,
frictionStatic: 1.4,
restitution: 0.18,
chamfer: { radius: Math.min(8, h / 2) },
label: "ash-button",
});
const lipW = Math.max(10, w * 0.1);
const lipH = 7;
const leftLip = Bodies.rectangle(
rect.left + lipW / 2,
rect.top - lipH / 2 + 1,
lipW,
lipH,
{
isStatic: true,
friction: 1.4,
restitution: 0.04,
label: "ash-lip",
},
);
const rightLip = Bodies.rectangle(
rect.right - lipW / 2,
rect.top - lipH / 2 + 1,
lipW,
lipH,
{
isStatic: true,
friction: 1.4,
restitution: 0.04,
label: "ash-lip",
},
);
World.add(engine.world, [buttonBody, leftLip, rightLip]);
const meta = new Map<number, ParticleMeta>();
const now = performance.now();
const count = Math.max(12, options.particleCount);
for (let i = 0; i < count; i++) {
const kindRoll = Math.random();
const kind: ParticleKind =
kindRoll > 0.75 ? "shard" : kindRoll > 0.4 ? "square" : "circle";
const size =
kind === "shard" ? 2.5 + Math.random() * 3 : 1.6 + Math.random() * 2.6;
const halfSpread = (options.spread * Math.PI) / 180 / 2;
const angle = -Math.PI / 2 + (Math.random() * 2 - 1) * halfSpread;
// Balanced kick: readable burst without flying off too hard.
const speed =
options.startVelocity * (0.45 + Math.random() * 0.55) * 0.7;
// Mostly explode from center; a few rain down later to settle on top.
const rain = Math.random() > 0.72;
const spawnX = rain
? rect.left + Math.random() * w
: cx + (Math.random() - 0.5) * w * 0.4;
const spawnY = rain
? rect.top - 8 - Math.random() * 28
: cy + (Math.random() - 0.5) * h * 0.3;
const body =
kind === "circle"
? Bodies.circle(spawnX, spawnY, size * 0.85, {
restitution: 0.35 + Math.random() * 0.3,
friction: 0.65 + Math.random() * 0.4,
frictionAir: 0.008 + Math.random() * 0.012,
density: 0.0012,
label: "ash-particle",
})
: Bodies.rectangle(
spawnX,
spawnY,
size * (kind === "shard" ? 1.4 : 1.6),
size * (kind === "shard" ? 2.2 : 1.6),
{
restitution: 0.3 + Math.random() * 0.28,
friction: 0.7 + Math.random() * 0.4,
frictionAir: 0.009 + Math.random() * 0.012,
density: 0.0013,
angle: Math.random() * Math.PI,
label: "ash-particle",
},
);
if (rain) {
Body.setVelocity(body, {
x: (Math.random() - 0.5) * 3.5,
y: 1 + Math.random() * 3,
});
} else {
Body.setVelocity(body, {
x: Math.cos(angle) * speed + (Math.random() - 0.5) * 4,
y: Math.sin(angle) * speed - (2 + Math.random() * 5),
});
}
Body.setAngularVelocity(body, (Math.random() - 0.5) * 0.55);
const color = pick(options.colors);
meta.set(body.id, {
color,
glow: isEmber(color),
kind,
size,
born: now,
life: 3200 + Math.random() * 2200,
});
World.add(engine.world, body);
}
return {
engine,
buttonBody,
leftLip,
rightLip,
meta,
raf: 0,
canvas,
ctx,
startedAt: now,
lastW: w,
lastH: h,
};
}
function paintSim(sim: AshSim, button: HTMLElement) {
const { ctx, canvas, engine, meta } = sim;
const w = canvas.clientWidth;
const h = canvas.clientHeight;
syncColliders(sim, button);
Engine.update(engine, 1000 / 60);
ctx.clearRect(0, 0, w, h);
const now = performance.now();
const toRemove: Matter.Body[] = [];
for (const body of Composite.allBodies(engine.world)) {
if (body.label !== "ash-particle") continue;
const info = meta.get(body.id);
if (!info) continue;
const age = now - info.born;
const fade = Math.max(0, 1 - age / info.life);
if (
fade <= 0 ||
body.position.y > h + 48 ||
body.position.x < -48 ||
body.position.x > w + 48
) {
toRemove.push(body);
continue;
}
ctx.save();
ctx.translate(body.position.x, body.position.y);
ctx.rotate(body.angle);
ctx.globalAlpha = 0.4 + fade * 0.6;
if (info.glow) {
ctx.globalAlpha = fade * 0.22;
ctx.fillStyle = info.color;
ctx.beginPath();
ctx.arc(0, 0, info.size * 2, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 0.4 + fade * 0.6;
}
if (info.kind === "shard") {
drawShard(ctx, info.size, info.color);
} else if (info.kind === "square") {
ctx.fillStyle = info.color;
ctx.fillRect(-info.size, -info.size, info.size * 2, info.size * 2);
} else {
ctx.fillStyle = info.color;
ctx.beginPath();
ctx.arc(0, 0, info.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
for (const body of toRemove) {
meta.delete(body.id);
World.remove(engine.world, body);
}
return meta.size > 0 && now - sim.startedAt < 6500;
}
export const AshBurstButton = React.forwardRef<
HTMLButtonElement,
AshBurstButtonProps
>(
(
{
children,
label = "Delete",
particleCount = 80,
spread = 110,
startVelocity = 42,
colors = ASH_COLORS,
icon = true,
onDestroy,
onClick,
className,
disabled,
type = "button",
...props
},
ref,
) => {
const buttonRef = React.useRef<HTMLButtonElement | null>(null);
const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
const simRef = React.useRef<AshSim | null>(null);
const fireTimerRef = React.useRef<number | undefined>(undefined);
const [simActive, setSimActive] = React.useState(false);
const [firing, setFiring] = React.useState(false);
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
const setButtonRef = (node: HTMLButtonElement | null) => {
buttonRef.current = node;
if (typeof ref === "function") ref(node);
else if (ref) ref.current = node;
};
const clearSim = (sim: AshSim) => {
cancelAnimationFrame(sim.raf);
World.clear(sim.engine.world, false);
Engine.clear(sim.engine);
sim.ctx.clearRect(0, 0, sim.canvas.clientWidth, sim.canvas.clientHeight);
};
const stopSim = React.useCallback(() => {
if (simRef.current) clearSim(simRef.current);
simRef.current = null;
setSimActive(false);
}, []);
React.useEffect(
() => () => {
stopSim();
window.clearTimeout(fireTimerRef.current);
},
[stopSim],
);
const startPhysicsBurst = () => {
const button = buttonRef.current;
const canvas = canvasRef.current;
if (!button || !canvas) return;
if (simRef.current) clearSim(simRef.current);
setSimActive(true);
const sim = createAshSimulation(canvas, button, {
particleCount,
spread,
startVelocity,
colors,
});
simRef.current = sim;
const tick = () => {
const current = simRef.current;
const btn = buttonRef.current;
if (!current || !btn) return;
if (paintSim(current, btn)) {
current.raf = requestAnimationFrame(tick);
} else {
stopSim();
}
};
sim.raf = requestAnimationFrame(tick);
};
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event);
if (disabled || event.defaultPrevented) return;
const reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
if (!reduceMotion) startPhysicsBurst();
setFiring(true);
window.clearTimeout(fireTimerRef.current);
fireTimerRef.current = window.setTimeout(() => setFiring(false), FIRE_MS);
onDestroy?.();
};
const overlay = mounted
? createPortal(
<canvas
ref={canvasRef}
aria-hidden
className="pointer-events-none fixed inset-0 z-[80]"
style={{ opacity: simActive ? 1 : 0 }}
/>,
document.body,
)
: null;
return (
<>
{overlay}
<button
ref={setButtonRef}
type={type}
disabled={disabled}
data-firing={firing || undefined}
onClick={handleClick}
className={cn(
"group/ash inline-flex h-9 cursor-pointer items-center justify-center gap-2 rounded-[10px] px-4 text-sm font-medium text-neutral-50 outline-none select-none",
// Graded dark surface: dark outer hairline, faint inner ring, top highlight, soft drop.
"bg-linear-to-b from-[#353535] to-[#272727] shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.06),inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.25),0_4px_12px_-4px_rgb(0_0_0/0.4)]",
"transition-[scale,filter,box-shadow] duration-300 ease-[cubic-bezier(0.34,1.35,0.64,1)] hover:brightness-110",
// Press eases in fast and settles the shadow; release springs back on the slower base curve.
"active:scale-[0.97] active:brightness-95 active:duration-100 active:ease-out active:shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.05),inset_0_1px_0_rgb(255_255_255/0.08),0_0_1px_rgb(0_0_0/0.2),0_1px_3px_-2px_rgb(0_0_0/0.3)] motion-reduce:active:scale-100",
// A burst leaves a faint ember glow on the face that cools back to the idle surface.
"data-firing:shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(249_115_22/0.18),inset_0_1px_0_rgb(255_255_255/0.14),inset_0_-8px_16px_-8px_rgb(239_68_68/0.35),0_1px_2px_rgb(0_0_0/0.25),0_4px_16px_-4px_rgb(239_68_68/0.35)]",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
icon && "pl-3",
className,
)}
{...props}
>
{icon ? (
<svg
aria-hidden
viewBox="0 0 16 16"
fill="currentColor"
className={cn(
"size-3.5 text-neutral-50/45 transition-[translate,scale,color] duration-300 ease-[cubic-bezier(0.34,1.35,0.64,1)]",
"group-hover/ash:text-red-400/80",
"group-data-firing/ash:-translate-y-px group-data-firing/ash:scale-110 group-data-firing/ash:text-orange-400 motion-reduce:transform-none",
)}
>
<path d="M8.6 1.2c.3 2-.6 3.1-1.6 4.2C5.9 6.6 4.5 8 4.5 10.2 4.5 12.6 6.2 14.5 8 14.5s3.5-1.6 3.5-4.1c0-1.5-.6-2.6-1.3-3.4-.1 1.1-.6 1.8-1.4 2.1.5-2.7-.1-5.8-2.2-7.9Z" />
</svg>
) : null}
<span>{children ?? label}</span>
</button>
</>
);
},
);
AshBurstButton.displayName = "AshBurstButton";
export default AshBurstButton;
lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
demo.tsx
export { ButtonDemo as default } from "@/import-examples/ash-burst-button";CSS
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--color-red-400: oklch(70.4% 0.191 22.216);
--color-orange-400: oklch(75% 0.183 55.934);
--color-neutral-50: oklch(98.5% 0 none);
--spacing: 0.25rem;
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--font-weight-medium: 500;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
--default-mono-font-family: var(--font-geist-mono);
}
}
@layer base {
*, ::after, ::before, ::backdrop, ::file-selector-button {
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0 solid;
}
html, :host {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
font-feature-settings: var(--default-font-feature-settings, normal);
font-variation-settings: var(--default-font-variation-settings, normal);
-webkit-tap-highlight-color: transparent;
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
h1, h2, h3, h4, h5, h6 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
-webkit-text-decoration: inherit;
text-decoration: inherit;
}
b, strong {
font-weight: bolder;
}
code, kbd, samp, pre {
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
font-feature-settings: var(--default-mono-font-feature-settings, normal);
font-variation-settings: var(--default-mono-font-variation-settings, normal);
font-size: 1em;
}
small {
font-size: 80%;
}
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
:-moz-focusring:where(:not(iframe)) {
outline: auto;
}
progress {
vertical-align: baseline;
}
summary {
display: list-item;
}
ol, ul, menu {
list-style: none;
}
img, svg, video, canvas, audio, iframe, embed, object {
display: block;
vertical-align: middle;
}
img, video {
max-width: 100%;
height: auto;
}
button, input, select, optgroup, textarea, ::file-selector-button {
font: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
border-radius: 0;
background-color: transparent;
opacity: 1;
}
:where(select:is([multiple], [size])) optgroup {
font-weight: bolder;
}
:where(select:is([multiple], [size])) optgroup option {
padding-inline-start: 20px;
}
::file-selector-button {
margin-inline-end: 4px;
}
::placeholder {
opacity: 1;
}
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
::placeholder {
color: currentcolor;
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, currentcolor 50%, transparent);
}
}
}
textarea {
resize: vertical;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-date-and-time-value {
min-height: 1lh;
text-align: inherit;
}
::-webkit-datetime-edit {
display: inline-flex;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
::-webkit-calendar-picker-indicator {
line-height: 1;
}
:-moz-ui-invalid {
box-shadow: none;
}
button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
appearance: button;
}
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
height: auto;
}
[hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}
}
@layer utilities {
.pointer-events-none {
pointer-events: none;
}
.fixed {
position: fixed;
}
.inset-0 {
inset: 0px;
}
.z-\[80\] {
z-index: 80;
}
.inline-flex {
display: inline-flex;
}
.size-3\.5 {
width: calc(var(--spacing) * 3.5);
height: calc(var(--spacing) * 3.5);
}
.h-9 {
height: calc(var(--spacing) * 9);
}
.cursor-pointer {
cursor: pointer;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.rounded-\[10px\] {
border-radius: 10px;
}
.bg-linear-to-b {
--tw-gradient-position: to bottom;
@supports (background-image: linear-gradient(in lab, red, red)) {
--tw-gradient-position: to bottom in oklab;
}
background-image: linear-gradient(var(--tw-gradient-stops));
}
.from-\[\#353535\] {
--tw-gradient-from: #353535;
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-\[\#272727\] {
--tw-gradient-to: #272727;
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.pl-3 {
padding-left: calc(var(--spacing) * 3);
}
.text-sm {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.text-neutral-50 {
color: var(--color-neutral-50);
}
.text-neutral-50\/45 {
color: color-mix(in srgb, oklch(98.5% 0 none) 45%, transparent);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-neutral-50) 45%, transparent);
}
}
.shadow-\[0_0_0_1px_rgb\(0_0_0\/0\.9\)\,inset_0_0_0_1px_rgb\(255_255_255\/0\.06\)\,inset_0_1px_0_rgb\(255_255_255\/0\.14\)\,0_1px_2px_rgb\(0_0_0\/0\.25\)\,0_4px_12px_-4px_rgb\(0_0_0\/0\.4\)\] {
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, rgb(0 0 0/0.9)), inset 0 0 0 1px var(--tw-shadow-color, rgb(255 255 255/0.06)), inset 0 1px 0 var(--tw-shadow-color, rgb(255 255 255/0.14)), 0 1px 2px var(--tw-shadow-color, rgb(0 0 0/0.25)), 0 4px 12px -4px var(--tw-shadow-color, rgb(0 0 0/0.4));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.transition-\[scale\,filter\,box-shadow\] {
transition-property: scale,filter,box-shadow;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.transition-\[translate\,scale\,color\] {
transition-property: translate,scale,color;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-300 {
--tw-duration: 300ms;
transition-duration: 300ms;
}
.ease-\[cubic-bezier\(0\.34\,1\.35\,0\.64\,1\)\] {
--tw-ease: cubic-bezier(0.34,1.35,0.64,1);
transition-timing-function: cubic-bezier(0.34,1.35,0.64,1);
}
.outline-none {
--tw-outline-style: none;
outline-style: none;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
@media (hover: hover) {
.group-hover\/ash\:text-red-400\/80:is(:where(.group\/ash):hover *) {
color: color-mix(in srgb, oklch(70.4% 0.191 22.216) 80%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.group-hover\/ash\:text-red-400\/80:is(:where(.group\/ash):hover *) {
color: color-mix(in oklab, var(--color-red-400) 80%, transparent);
}
}
}
.group-data-firing\/ash\:-translate-y-px:is(:where(.group\/ash)[data-firing] *) {
--tw-translate-y: -1px;
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.group-data-firing\/ash\:scale-110:is(:where(.group\/ash)[data-firing] *) {
--tw-scale-x: 110%;
--tw-scale-y: 110%;
--tw-scale-z: 110%;
scale: var(--tw-scale-x) var(--tw-scale-y);
}
.group-data-firing\/ash\:text-orange-400:is(:where(.group\/ash)[data-firing] *) {
color: var(--color-orange-400);
}
@media (hover: hover) {
.hover\:brightness-110:hover {
--tw-brightness: brightness(110%);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
}
.focus-visible\:ring-2:focus-visible {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.focus-visible\:ring-ring:focus-visible {
--tw-ring-color: var(--ring);
}
.focus-visible\:ring-offset-2:focus-visible {
--tw-ring-offset-width: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
.focus-visible\:ring-offset-background:focus-visible {
--tw-ring-offset-color: var(--background);
}
.active\:scale-\[0\.97\]:active {
scale: 0.97;
}
.active\:shadow-\[0_0_0_1px_rgb\(0_0_0\/0\.9\)\,inset_0_0_0_1px_rgb\(255_255_255\/0\.05\)\,inset_0_1px_0_rgb\(255_255_255\/0\.08\)\,0_0_1px_rgb\(0_0_0\/0\.2\)\,0_1px_3px_-2px_rgb\(0_0_0\/0\.3\)\]:active {
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, rgb(0 0 0/0.9)), inset 0 0 0 1px var(--tw-shadow-color, rgb(255 255 255/0.05)), inset 0 1px 0 var(--tw-shadow-color, rgb(255 255 255/0.08)), 0 0 1px var(--tw-shadow-color, rgb(0 0 0/0.2)), 0 1px 3px -2px var(--tw-shadow-color, rgb(0 0 0/0.3));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.active\:brightness-95:active {
--tw-brightness: brightness(95%);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.active\:duration-100:active {
--tw-duration: 100ms;
transition-duration: 100ms;
}
.active\:ease-out:active {
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
}
.disabled\:cursor-not-allowed:disabled {
cursor: not-allowed;
}
.disabled\:opacity-50:disabled {
opacity: 50%;
}
.data-firing\:shadow-\[0_0_0_1px_rgb\(0_0_0\/0\.9\)\,inset_0_0_0_1px_rgb\(249_115_22\/0\.18\)\,inset_0_1px_0_rgb\(255_255_255\/0\.14\)\,inset_0_-8px_16px_-8px_rgb\(239_68_68\/0\.35\)\,0_1px_2px_rgb\(0_0_0\/0\.25\)\,0_4px_16px_-4px_rgb\(239_68_68\/0\.35\)\][data-firing] {
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, rgb(0 0 0/0.9)), inset 0 0 0 1px var(--tw-shadow-color, rgb(249 115 22/0.18)), inset 0 1px 0 var(--tw-shadow-color, rgb(255 255 255/0.14)), inset 0 -8px 16px -8px var(--tw-shadow-color, rgb(239 68 68/0.35)), 0 1px 2px var(--tw-shadow-color, rgb(0 0 0/0.25)), 0 4px 16px -4px var(--tw-shadow-color, rgb(239 68 68/0.35));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
@media (prefers-reduced-motion: reduce) {
.motion-reduce\:transform-none {
transform: none;
}
.motion-reduce\:active\:scale-100:active {
--tw-scale-x: 100%;
--tw-scale-y: 100%;
--tw-scale-z: 100%;
scale: var(--tw-scale-x) var(--tw-scale-y);
}
}
}
:root {
--background: #fff;
--foreground: #111;
--primary: #171717;
--primary-foreground: #fff;
--secondary: #eee;
--secondary-foreground: #171717;
--muted: #eee;
--muted-foreground: #666;
--accent: #eee;
--accent-foreground: #171717;
--border: #ddd;
--input: #ddd;
--ring: #777;
--destructive: #dc2626;
--destructive-foreground: #fff;
--popover: #fff;
--popover-foreground: #111;
--success: #15803d;
--success-foreground: #fff;
--radius: .625rem;
--hairline: 1px;
}
.dark {
--background: #090909;
--foreground: #fafafa;
--primary: #fafafa;
--primary-foreground: #171717;
--secondary: #262626;
--secondary-foreground: #fafafa;
--muted: #262626;
--muted-foreground: #a3a3a3;
--accent: #262626;
--accent-foreground: #fafafa;
--border: #333;
--input: #333;
--popover: #171717;
--popover-foreground: #fafafa;
}
:root {
--card: oklch(1 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--primary: oklch(0.205 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--chrome: oklch(0.967 0 0);
--brand: oklch(0.6 0.23 29);
--chart-background: oklch(1 0 0);
--chart-foreground: oklch(0.145 0.004 285);
--chart-foreground-muted: oklch(0.55 0.014 260);
--chart-line-primary: var(--chart-1);
--chart-line-secondary: var(--chart-2);
--chart-crosshair: oklch(0.4 0.1828 274.34);
--chart-grid: oklch(0.9 0 0);
--chart-brush-border: var(--chart-grid);
--chart-tooltip-background: oklch(0.21 0.006 285 / 0.8);
--chart-tooltip-foreground: oklch(0.985 0 0);
--chart-tooltip-muted: oklch(0.65 0.01 260);
--chart-marker-background: oklch(0.97 0.005 260);
--chart-marker-border: oklch(0.85 0.01 260);
--chart-marker-foreground: oklch(0.3 0.01 260);
--chart-label: oklch(0.45 0.01 260);
--chart-scale-01: oklch(0.98 0.003 106);
--chart-scale-02: oklch(0.92 0.008 106);
--chart-scale-03: oklch(0.82 0.015 106);
--chart-scale-04: oklch(0.68 0.02 106);
--chart-scale-05: oklch(0.55 0.025 106);
--chart-scale-pattern-color: oklch(0.96 0.005 106);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--chrome: oklch(0.18 0 0);
--card: oklch(0.205 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--border: oklch(1 0 0 / 10%);
--primary: oklch(0.922 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.922 0 0);
--sidebar-primary-foreground: oklch(0.205 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--brand: oklch(0.66 0.23 32);
--chart-background: oklch(0.145 0 0);
--chart-foreground: oklch(0.45 0 0);
--chart-foreground-muted: oklch(0.65 0.01 260);
--chart-crosshair: oklch(0.45 0 0);
--chart-grid: oklch(0.25 0 0);
--chart-brush-border: var(--chart-grid);
--chart-marker-background: oklch(0.25 0.01 260);
--chart-marker-border: oklch(0.4 0.01 260);
--chart-marker-foreground: oklch(0.9 0 0);
--chart-label: oklch(0.75 0.01 260);
--chart-scale-01: oklch(0.28 0.03 270);
--chart-scale-02: oklch(0.38 0.04 270);
--chart-scale-03: oklch(0.48 0.05 270);
--chart-scale-04: oklch(0.58 0.06 270);
--chart-scale-05: oklch(0.68 0.07 270);
--chart-scale-pattern-color: oklch(0.145 0 0);
}
@keyframes docs-dot-swing {
0%, 100% {
transform: translateX(0) scale(1);
}
45% {
transform: translateX(-6px) scale(0.7);
}
}
@keyframes hell-flicker {
0%, 100% {
opacity: 1;
}
41% {
opacity: 0.7;
}
43% {
opacity: 1;
}
77% {
opacity: 0.85;
}
78% {
opacity: 1;
}
}
@keyframes lights-off {
0% {
opacity: 0;
}
15% {
opacity: 1;
}
30% {
opacity: 0.2;
}
45% {
opacity: 1;
}
60% {
opacity: 0.5;
}
70%, 100% {
opacity: 1;
}
}
@keyframes lights-on {
0% {
opacity: 1;
}
20% {
opacity: 0;
}
32% {
opacity: 1;
}
50% {
opacity: 0.1;
}
60% {
opacity: 0.9;
}
75%, 100% {
opacity: 0;
}
}
@keyframes lights-fade {
to {
opacity: 0;
}
}
@keyframes eyes-dim {
from {
opacity: 0;
}
}
@keyframes eyes-open {
from {
opacity: 0;
transform: scaleY(0.05);
}
}
@keyframes eyes-blink {
0%, 92%, 100% {
transform: none;
}
95% {
transform: scaleY(0.08);
}
}
@keyframes crack-life {
0% {
opacity: 0;
transform: scale(0.92);
}
4% {
opacity: 1;
transform: none;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes egg-wobble {
0%, 84%, 100% {
transform: none;
}
87% {
transform: rotate(-14deg);
}
90% {
transform: rotate(10deg);
}
93% {
transform: rotate(-6deg);
}
96% {
transform: rotate(3deg);
}
}
@keyframes page-glitch {
0% {
transform: none;
filter: none;
clip-path: none;
}
12% {
transform: translate(-6px, 2px) skewX(-2deg);
filter: hue-rotate(90deg) saturate(3);
}
24% {
transform: translate(5px, -1px);
filter: invert(1) hue-rotate(180deg);
clip-path: inset(18% 0 52% 0);
}
36% {
transform: none;
filter: contrast(2.2) saturate(0);
clip-path: none;
}
48% {
transform: translate(9px, 0) skewX(4deg);
filter: hue-rotate(-60deg);
clip-path: inset(0 0 70% 0);
}
60% {
transform: translate(-3px, 3px);
filter: none;
clip-path: inset(40% 0 20% 0);
}
72% {
transform: translate(2px, -4px) scaleY(1.02);
filter: saturate(4) contrast(1.4);
clip-path: none;
}
86% {
transform: translate(-8px, 0);
filter: invert(1);
clip-path: inset(62% 0 8% 0);
}
}
@keyframes glitch-label {
0% {
transform: translate(0, 0);
opacity: 1;
}
33% {
transform: translate(-4px, 2px);
opacity: 0.6;
}
66% {
transform: translate(5px, -2px);
opacity: 1;
}
}
@keyframes deleted-progress {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes deleted-restore {
0%, 70% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes creeper-swell {
0% {
transform: scale(0.3) translateY(40px);
opacity: 0;
filter: none;
}
20% {
transform: scale(1);
opacity: 1;
}
45%, 65%, 85% {
filter: brightness(3) saturate(0);
}
55%, 75% {
filter: none;
}
100% {
transform: scale(1.35);
filter: brightness(4) saturate(0);
}
}
@keyframes creeper-flash {
from {
opacity: 0.85;
}
to {
opacity: 0;
}
}
@property --tw-gradient-position {
syntax: "*";
inherits: false;
}
@property --tw-gradient-from {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-via {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-to {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-stops {
syntax: "*";
inherits: false;
}
@property --tw-gradient-via-stops {
syntax: "*";
inherits: false;
}
@property --tw-gradient-from-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 0%;
}
@property --tw-gradient-via-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 50%;
}
@property --tw-gradient-to-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-font-weight {
syntax: "*";
inherits: false;
}
@property --tw-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-inset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-ring-inset {
syntax: "*";
inherits: false;
}
@property --tw-ring-offset-width {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
@property --tw-ring-offset-color {
syntax: "*";
inherits: false;
initial-value: #fff;
}
@property --tw-ring-offset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-duration {
syntax: "*";
inherits: false;
}
@property --tw-ease {
syntax: "*";
inherits: false;
}
@property --tw-translate-x {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-y {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-z {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-scale-x {
syntax: "*";
inherits: false;
initial-value: 1;
}
@property --tw-scale-y {
syntax: "*";
inherits: false;
initial-value: 1;
}
@property --tw-scale-z {
syntax: "*";
inherits: false;
initial-value: 1;
}
@property --tw-blur {
syntax: "*";
inherits: false;
}
@property --tw-brightness {
syntax: "*";
inherits: false;
}
@property --tw-contrast {
syntax: "*";
inherits: false;
}
@property --tw-grayscale {
syntax: "*";
inherits: false;
}
@property --tw-hue-rotate {
syntax: "*";
inherits: false;
}
@property --tw-invert {
syntax: "*";
inherits: false;
}
@property --tw-opacity {
syntax: "*";
inherits: false;
}
@property --tw-saturate {
syntax: "*";
inherits: false;
}
@property --tw-sepia {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-drop-shadow-size {
syntax: "*";
inherits: false;
}
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
--tw-gradient-position: initial;
--tw-gradient-from: #0000;
--tw-gradient-via: #0000;
--tw-gradient-to: #0000;
--tw-gradient-stops: initial;
--tw-gradient-via-stops: initial;
--tw-gradient-from-position: 0%;
--tw-gradient-via-position: 50%;
--tw-gradient-to-position: 100%;
--tw-font-weight: initial;
--tw-shadow: 0 0 #0000;
--tw-shadow-color: initial;
--tw-shadow-alpha: 100%;
--tw-inset-shadow: 0 0 #0000;
--tw-inset-shadow-color: initial;
--tw-inset-shadow-alpha: 100%;
--tw-ring-color: initial;
--tw-ring-shadow: 0 0 #0000;
--tw-inset-ring-color: initial;
--tw-inset-ring-shadow: 0 0 #0000;
--tw-ring-inset: initial;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-duration: initial;
--tw-ease: initial;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-translate-z: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-scale-z: 1;
--tw-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;
--tw-grayscale: initial;
--tw-hue-rotate: initial;
--tw-invert: initial;
--tw-opacity: initial;
--tw-saturate: initial;
--tw-sepia: initial;
--tw-drop-shadow: initial;
--tw-drop-shadow-color: initial;
--tw-drop-shadow-alpha: 100%;
--tw-drop-shadow-size: initial;
}
}
}