Exploding Input
The ExplodingInput component is a delightful, interactive input field that renders performant canvas-based particle explosions as the user types.
Typed Characters (Letters)
Cursor Trail
- Preview
- Code
import { ExplodingInput } from '@ignix-ui/exploding-input';
<ExplodingInput
placeholder="Type here..."
particlePreset="confetti"
triggerMode="keypress"
direction="up"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
Installation
- CLI
- Manual
ignix add component exploding-input
import * as React from 'react';
import { createPortal } from 'react-dom';
import { cn } from '../../../utils/cn';
//Types
type TriggerMode = "keypress" | "submit" | "focus" | "clear" | "custom";
type ParticlePreset = "confetti" | "sparks" | "stars" | "bubbles" | "letters" | "emoji";
type Direction = "up" | "down" | "left" | "right" | "radial" | "burst";
type AudioPreset = "pop" | "whoosh" | "sparkle";
interface Particle {
id: number;
x: number;
y: number;
vx: number;
vy: number;
spawnedAt: number;
maxLifeMs: number;
size: number;
rotation: number;
rotationSpeed: number;
opacity: number;
color: string;
content: string;
type: ParticlePreset;
scale: number;
}
export interface ExplodingInputProps extends Omit<React.ComponentProps<"input">, "validate"> {
triggerMode?: TriggerMode;
particlePreset?: ParticlePreset;
customEmoji?: string[];
characterParticles?: boolean;
direction?: Direction;
cursorTrail?: boolean;
validate?: (value: string) => boolean;
maxParticles?: number;
audio?: AudioPreset | string;
explodeRef?: React.Ref<ExplodingInputHandle>;
}
export interface ExplodingInputHandle {
explode: () => void;
}
//Preset Definitions
const CONFETTI_COLORS = [
"hsla(0, 74%, 58%, 1.00)",
"hsla(45, 76%, 66%, 1.00)",
"hsla(120, 69%, 63%, 1.00)",
"hsla(200, 79%, 65%, 1.00)",
"hsla(280, 81%, 71%, 1.00)",
"hsla(330, 79%, 67%, 1.00)",
];
const SPARK_COLORS = [
"hsl(40 100% 70%)",
"hsl(30 100% 60%)",
"hsl(50 100% 80%)",
"hsl(20 100% 55%)",
];
const STAR_COLORS = [
"hsl(45 100% 60%)",
"hsl(45 80% 80%)",
"hsl(0 0% 95%)"
];
const BUBBLE_COLORS = [
"hsla(200 80% 70% / 0.6)",
"hsla(220 70% 75% / 0.5)",
"hsla(180 60% 65% / 0.5)",
"hsla(260 50% 75% / 0.4)",
]
const LETTER_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%";
const DEFAULT_EMOJI = ["🤩", "👾", "😺", "👻", "🎃", "🖤", "🗯️", "✨", "🎉", "💥"];
function randomFrom<T>(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)];
}
function getPresetParticle(
preset: ParticlePreset,
char?: string,
customEmoji?: string[]
): { content: string; color: string; size: number } {
switch (preset) {
case "confetti":
return { content: "■", color: randomFrom(CONFETTI_COLORS), size: 8 + Math.random() * 6 };
case "sparks":
return { content: "•", color: randomFrom(SPARK_COLORS), size: 4 + Math.random() * 4 };
case "stars":
return { content: "★", color: randomFrom(STAR_COLORS), size: 10 + Math.random() * 8 };
case "bubbles":
return { content: "●", color: randomFrom(BUBBLE_COLORS), size: 8 + Math.random() * 12 };
case "letters":
return {
content: char || randomFrom(LETTER_CHARS.split("")),
color: randomFrom(CONFETTI_COLORS),
size: 18 + Math.random() * 6,
};
case "emoji":
return {
content: randomFrom(customEmoji?.length ? customEmoji : DEFAULT_EMOJI),
color: "#000000",
size: 22 + Math.random() * 14,
};
}
}
//Direction Vectors
function getDirectionVelocity(
direction: Direction,
speed: number,
spreadFactor = 0.4
): { vx: number; vy: number } {
const spread = () => (Math.random() - 0.5) * speed * spreadFactor;
switch (direction) {
case "up":
return { vx: spread(), vy: -(speed * 0.5 + Math.random() * speed) };
case "down":
return { vx: spread(), vy: speed * 0.5 + Math.random() * speed };
case "left":
return { vx: -(speed * 0.5 + Math.random() * speed), vy: spread() };
case "right":
return { vx: speed * 0.5 + Math.random() * speed, vy: spread() };
case "radial": {
const angle = Math.random() * Math.PI * 2;
const mag = speed * 0.5 + Math.random() * speed * 0.8;
return { vx: Math.cos(angle) * mag, vy: Math.sin(angle) * mag };
}
case "burst": {
const a = Math.random() * Math.PI * 2;
const m = speed * 0.8 + Math.random() * speed;
return { vx: Math.cos(a) * m, vy: Math.sin(a) * m };
}
}
}
//Typing speed tracker
class TypingSpeedTracker {
private timestamps: number[] = [];
private windowSize = 5;
record() {
this.timestamps.push(performance.now());
if (this.timestamps.length > this.windowSize + 1) {
this.timestamps.shift();
}
}
getIntensity(): number {
if (this.timestamps.length < 2) return 0.3;
const intervals: number[] = [];
for (let i = 1; i < this.timestamps.length; i++) {
intervals.push(this.timestamps[i] - this.timestamps[i - 1]);
}
const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length;
const clamped = Math.max(50, Math.min(500, avg));
return 1.0 - (clamped - 50) / 450;
}
getParticleCount(base = 8): number {
const intensity = this.getIntensity();
return Math.max(3, Math.round(base * (0.3 + intensity * 2.0)));
}
getSpeedMultiplier(): number {
const intensity = this.getIntensity();
return 0.5 + intensity * 2.5;
}
}
//Audio Manager
class AudioManager {
private ctx: AudioContext | null = null;
private customBuffer: AudioBuffer | null = null;
private preset: AudioPreset | string;
private loadFailed = false;
constructor(preset: AudioPreset | string) {
this.preset = preset;
if (!this.isBuiltinPreset(preset)) {
this.loadCustom(preset);
}
}
private isBuiltinPreset(p: string): p is AudioPreset {
return p === "pop" || p === "sparkle" || p === "whoosh";
}
private getCtx(): AudioContext {
if (!this.ctx) {
if (typeof window === "undefined" || !window.AudioContext) {
throw new Error("Web Audio API is not available");
}
this.ctx = new AudioContext();
}
return this.ctx;
}
private async loadCustom(url: string) {
try {
const ctx = this.getCtx();
const res = await fetch(url);
if (!res.ok) { this.loadFailed = true; return; }
const buf = await res.arrayBuffer();
this.customBuffer = await ctx.decodeAudioData(buf);
} catch {
this.loadFailed = true;
}
}
play() {
if (this.loadFailed && !this.isBuiltinPreset(this.preset)) return;
try {
if (this.isBuiltinPreset(this.preset)) {
this.playBuiltin(this.preset);
} else if (this.customBuffer) {
this.playBuffer(this.customBuffer);
}
} catch {
if (this.preset !== "pop") this.playBuiltin("pop");
}
}
private playBuffer(buffer: AudioBuffer) {
const ctx = this.getCtx();
const source = ctx.createBufferSource();
source.buffer = buffer;
const gain = ctx.createGain();
gain.gain.value = 0.15;
source.connect(gain).connect(ctx.destination);
source.start();
}
private playBuiltin(preset: AudioPreset) {
const ctx = this.getCtx();
const now = ctx.currentTime;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
gain.gain.value = 0.08;
switch (preset) {
case "pop":
osc.type = "sine";
osc.frequency.setValueAtTime(600, now);
osc.frequency.exponentialRampToValueAtTime(200, now + 0.08);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.1);
break;
case "sparkle":
osc.type = "sine";
osc.frequency.setValueAtTime(1200, now);
osc.frequency.exponentialRampToValueAtTime(2400, now + 0.05);
osc.frequency.exponentialRampToValueAtTime(800, now + 0.15);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.18);
break;
case "whoosh": {
osc.type = "sawtooth";
osc.frequency.setValueAtTime(100, now);
osc.frequency.exponentialRampToValueAtTime(800, now + 0.15);
osc.frequency.exponentialRampToValueAtTime(50, now + 0.3);
gain.gain.setValueAtTime(0.04, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
const filter = ctx.createBiquadFilter();
filter.type = "lowpass";
filter.frequency.value = 1500;
osc.connect(filter).connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.3);
break;
}
}
}
dispose() {
if (this.ctx) {
this.ctx.close().catch(Object);
this.ctx = null;
}
}
}
function drawStar(ctx: CanvasRenderingContext2D, r: number) {
const inner = r * 0.4;
ctx.beginPath();
for (let i = 0; i < 10; i++) {
const a = (i * Math.PI) / 5 - Math.PI / 2;
const rad = i % 2 === 0 ? r : inner;
ctx.lineTo(Math.cos(a) * rad, Math.sin(a) * rad);
}
ctx.closePath();
ctx.fill();
}
class ParticleEngine {
private particles: Particle[] = [];
private nextId = 0;
private canvas: HTMLCanvasElement;
private ctx2d: CanvasRenderingContext2D;
private animFrame: number | null = null;
private maxParticles: number;
private running = false;
private dpr = 1;
private textureCache = new Map<string, HTMLCanvasElement>();
customEmoji: string[] = [];
constructor(canvas: HTMLCanvasElement, maxParticles: number) {
this.canvas = canvas;
this.ctx2d = canvas.getContext("2d")!;
this.maxParticles = Math.max(0, Math.floor(maxParticles || 0));
}
setDpr(dpr: number) {
this.dpr = dpr;
this.ctx2d.setTransform(dpr, 0, 0, dpr, 0, 0);
}
spawn(
x: number,
y: number,
count: number,
preset: ParticlePreset,
direction: Direction,
speedMultiplier: number,
colorOverride?: string,
char?: string
) {
if (this.maxParticles <= 0) return;
for (let i = 0; i < count; i++) {
if (this.particles.length >= this.maxParticles) {
this.particles.shift();
}
const p = getPresetParticle(preset, char, this.customEmoji);
const velocityMultiplier = preset === "emoji"
? 2.5 * speedMultiplier * (1.5 + Math.random() * 1.5)
: 2 * speedMultiplier * (1.0 + Math.random() * 1.0);
const spreadFactor = (preset === "emoji" || preset === "letters") ? 2.5 : (preset === "stars" || preset === "confetti") ? 1.4 : 1.2;
const vel = getDirectionVelocity(direction, velocityMultiplier, spreadFactor);
const maxLifeMs = preset === "letters"
? 1500 + Math.random() * 1000
: 4000 + Math.random() * 2000;
this.particles.push({
id: this.nextId++,
x,
y,
vx: vel.vx,
vy: vel.vy,
spawnedAt: performance.now(),
maxLifeMs,
size: p.size,
rotation: Math.random() * 360,
rotationSpeed: preset === "emoji"
? (Math.random() - 0.5) * 4
: (Math.random() - 0.5) * 10,
opacity: 1,
color: colorOverride || p.color,
content: p.content,
type: preset,
scale: 1,
});
}
if (!this.running) this.start();
}
private start() {
this.running = true;
const loop = (now: number) => {
this.update(now);
this.render();
if (this.particles.length > 0) {
this.animFrame = requestAnimationFrame(loop);
} else {
this.running = false;
}
};
this.animFrame = requestAnimationFrame(loop);
}
private getTexture(p: Particle): HTMLCanvasElement {
const roundedSize = Math.round(p.size);
const key = `${p.content}-${p.color}-${roundedSize}-${this.dpr}`;
if (this.textureCache.has(key)) return this.textureCache.get(key)!;
if (this.textureCache.size >= 50) {
this.textureCache.clear();
}
const offCanvas = document.createElement("canvas");
const renderSize = roundedSize * 1.5;
offCanvas.width = renderSize * this.dpr;
offCanvas.height = renderSize * this.dpr;
const offCtx = offCanvas.getContext("2d")!;
offCtx.scale(this.dpr, this.dpr);
offCtx.fillStyle = p.color;
offCtx.font = `${roundedSize}px sans-serif`;
offCtx.textAlign = "center";
offCtx.textBaseline = "middle";
offCtx.fillText(p.content, renderSize / 2, renderSize / 2);
this.textureCache.set(key, offCanvas);
return offCanvas;
}
private update(now: number) {
const gravity = 0.15;
const friction = 0.97;
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
const elapsed = now - p.spawnedAt;
const lifeRatio = Math.min(elapsed / p.maxLifeMs, 1);
p.x += p.vx;
p.y += p.vy;
p.vy += gravity;
p.vx *= friction;
p.vy *= friction;
p.rotation += p.rotationSpeed;
if (p.type === "emoji" || p.type === "confetti") {
p.opacity = 1;
p.scale = 1;
} else {
p.opacity = 1 - lifeRatio;
p.scale = 1 - lifeRatio * 0.3;
}
if (lifeRatio >= 1) {
this.particles.splice(i, 1);
}
}
}
private render() {
const cssW = this.canvas.width / this.dpr;
const cssH = this.canvas.height / this.dpr;
this.ctx2d.clearRect(0, 0, cssW, cssH);
for (const p of this.particles) {
this.ctx2d.save();
this.ctx2d.translate(p.x, p.y);
this.ctx2d.rotate((p.rotation * Math.PI) / 180);
this.ctx2d.globalAlpha = p.opacity;
this.ctx2d.fillStyle = p.color;
if (p.type === "sparks") {
this.ctx2d.shadowBlur = 6;
this.ctx2d.shadowColor = p.color;
}
const s = p.size * p.scale;
if (p.type === "stars") {
drawStar(this.ctx2d, s * 0.55);
} else if (p.type === "confetti") {
this.ctx2d.fillRect(-s / 2, -s / 2, s, s);
} else if (p.type === "sparks" || p.type === "bubbles") {
this.ctx2d.beginPath();
this.ctx2d.arc(0, 0, s / 2, 0, Math.PI * 2);
this.ctx2d.fill();
} else {
const texture = this.getTexture(p);
const drawW = texture.width / this.dpr;
const drawH = texture.height / this.dpr;
this.ctx2d.drawImage(
texture,
-(drawW * p.scale) / 2,
-(drawH * p.scale) / 2,
drawW * p.scale,
drawH * p.scale
);
}
this.ctx2d.shadowBlur = 0;
this.ctx2d.restore();
}
}
clear() {
this.particles = [];
if (this.animFrame) {
cancelAnimationFrame(this.animFrame);
this.animFrame = null;
}
this.running = false;
this.ctx2d.clearRect(0, 0, this.canvas.width / this.dpr, this.canvas.height / this.dpr);
}
dispose() {
this.clear();
this.textureCache.clear();
}
}
let _measureSpan: HTMLSpanElement | null = null;
function getMeasureSpan(input: HTMLInputElement): HTMLSpanElement {
if (!_measureSpan) {
_measureSpan = document.createElement("span");
_measureSpan.style.cssText =
"visibility:hidden;position:absolute;white-space:pre;pointer-events:none;top:-9999px;left:0";
document.body.appendChild(_measureSpan);
}
const s = window.getComputedStyle(input);
_measureSpan.style.font = s.font;
_measureSpan.style.letterSpacing = s.letterSpacing;
return _measureSpan;
}
function getCursorPixelPosition(
input: HTMLInputElement
): { x: number; y: number } {
const inputRect = input.getBoundingClientRect();
const span = getMeasureSpan(input);
const pos = input.selectionStart ?? input.value.length;
span.textContent = input.value.substring(0, pos);
const textWidth = span.offsetWidth;
const style = window.getComputedStyle(input);
const paddingLeft = parseFloat(style.paddingLeft) || 0;
const scrollLeft = input.scrollLeft || 0;
return {
x: inputRect.left + paddingLeft + textWidth - scrollLeft,
y: inputRect.top + inputRect.height / 2,
};
}
//Reduced Motion Hook
function usePrefersReducedMotion(): boolean {
const [reduced, setReduced] = React.useState(() => {
if (typeof window === "undefined") return false;
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
});
React.useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
const handler = (e: MediaQueryListEvent) => setReduced(e.matches);
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, []);
return reduced;
}
//Component
const ExplodingInput = React.forwardRef<HTMLInputElement, ExplodingInputProps>(
(
{
className,
triggerMode = "keypress",
particlePreset = "confetti",
customEmoji,
characterParticles = false,
direction = "up",
cursorTrail = false,
validate,
maxParticles = 400,
audio,
explodeRef,
type,
onChange,
onFocus,
onKeyDown,
onBlur,
...props
},
ref
) => {
const reducedMotion = usePrefersReducedMotion();
const [mounted, setMounted] = React.useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const inputRef = React.useRef<HTMLInputElement>(null);
const engineRef = React.useRef<ParticleEngine | null>(null);
const audioRef = React.useRef<AudioManager | null>(null);
const speedTracker = React.useRef(new TypingSpeedTracker());
const trailTimer = React.useRef<number | null>(null);
const prevValueRef = React.useRef("");
React.useEffect(() => {
if (typeof props.value === 'string') {
prevValueRef.current = props.value;
}
}, [props.value]);
React.useImperativeHandle(ref, () => inputRef.current!);
React.useImperativeHandle(explodeRef, () => ({
explode: () => fireExplosion(undefined, 40, 2.5),
}));
React.useEffect(() => { setMounted(true); }, []);
React.useEffect(() => {
if (engineRef.current) {
engineRef.current.customEmoji = customEmoji ?? [];
}
}, [customEmoji]);
React.useEffect(() => {
if (reducedMotion || !mounted) return;
const canvas = canvasRef.current;
if (!canvas) return;
const engine = new ParticleEngine(canvas, maxParticles);
engine.customEmoji = customEmoji ?? [];
engineRef.current = engine;
const resize = () => {
const dpr = window.devicePixelRatio || 1;
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
canvas.style.width = `${window.innerWidth}px`;
canvas.style.height = `${window.innerHeight}px`;
engine.setDpr(dpr);
};
resize();
window.addEventListener('resize', resize);
return () => {
window.removeEventListener('resize', resize);
engineRef.current?.dispose();
engineRef.current = null;
};
}, [reducedMotion, maxParticles, mounted]);
React.useEffect(() => {
if (!audio || reducedMotion) {
audioRef.current?.dispose();
audioRef.current = null;
return;
}
audioRef.current = new AudioManager(audio);
return () => {
audioRef.current?.dispose();
audioRef.current = null;
};
}, [audio, reducedMotion]);
const getValidationColor = React.useCallback((): string | undefined => {
if (!validate || !inputRef.current) return undefined;
const valid = validate(inputRef.current.value);
return valid ? "hsl(140 70% 45%)" : "hsl(0 80% 55%)";
}, [validate]);
const fireExplosion = React.useCallback(
(char?: string, overrideCount?: number, overrideSpeed?: number) => {
if (reducedMotion || !engineRef.current || !inputRef.current) return;
const pos = getCursorPixelPosition(inputRef.current);
const preset = characterParticles && char ? "letters" : particlePreset;
const charToUse = characterParticles && char ? char : undefined;
let count: number;
if (overrideCount !== undefined) {
count = overrideCount;
} else {
const base = speedTracker.current.getParticleCount();
const jitter = preset === "confetti"
? Math.floor(Math.random() * 5) + 1
: Math.floor(Math.random() * 3) + 1;
count = base + jitter;
}
const speed = overrideSpeed ?? speedTracker.current.getSpeedMultiplier();
const color = particlePreset === "emoji" ? undefined : getValidationColor();
engineRef.current.spawn(pos.x, pos.y, count, preset, direction, speed, color, charToUse);
audioRef.current?.play();
},
[reducedMotion, characterParticles, particlePreset, direction, getValidationColor]
);
const fireTrailParticle = React.useCallback(() => {
if (reducedMotion || !engineRef.current || !inputRef.current) return;
const pos = getCursorPixelPosition(inputRef.current);
const color = particlePreset === "emoji" ? undefined : getValidationColor();
engineRef.current.spawn(pos.x, pos.y, 1, "sparks", "radial", 0.3, color);
}, [reducedMotion, particlePreset, getValidationColor]);
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
const prevValue = prevValueRef.current;
if (triggerMode === "keypress") {
speedTracker.current.record();
if (newValue.length > prevValue.length) {
const typed = newValue[newValue.length - 1];
fireExplosion(typed);
} else if (newValue.length < prevValue.length) {
fireExplosion();
}
}
if (triggerMode === "clear" && newValue.length < prevValue.length) {
speedTracker.current.record();
if (newValue === "") {
fireExplosion(undefined, 30, 2.0);
} else {
fireExplosion();
}
}
prevValueRef.current = newValue;
onChange?.(e);
},
[triggerMode, fireExplosion, onChange]
);
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (triggerMode === "submit" && e.key === "Enter") {
fireExplosion(undefined, 60, 2.8);
}
onKeyDown?.(e);
},
[triggerMode, fireExplosion, onKeyDown]
);
const handleFocus = React.useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
if (triggerMode === "focus") {
setTimeout(() => fireExplosion(undefined, 25, 1.5), 0);
}
if (cursorTrail && !reducedMotion) {
if (trailTimer.current) clearTimeout(trailTimer.current);
const trail = () => {
fireTrailParticle();
trailTimer.current = window.setTimeout(trail, 80);
};
trailTimer.current = window.setTimeout(trail, 80);
}
onFocus?.(e);
},
[triggerMode, cursorTrail, reducedMotion, fireExplosion, fireTrailParticle, onFocus]
);
const handleBlur = React.useCallback((e: React.FocusEvent<HTMLInputElement>) => {
if (trailTimer.current) {
clearTimeout(trailTimer.current);
trailTimer.current = null;
}
onBlur?.(e);
}, [onBlur]);
React.useEffect(() => {
return () => {
if (trailTimer.current) clearTimeout(trailTimer.current);
};
}, []);
return (
<div ref={containerRef} className="relative inline-block w-full overflow-visible">
{mounted && typeof document !== "undefined" && createPortal(
<canvas
ref={canvasRef}
aria-hidden="true"
className="pointer-events-none fixed inset-0 z-50"
/>,
document.body
)}
<input
ref={inputRef}
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
onChange={handleChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
{...props}
/>
</div>
);
}
);
ExplodingInput.displayName = "ExplodingInput";
export { ExplodingInput };
Usage
import { ExplodingInput } from '@ignix-ui/exploding-input';
Basic Usage
export default function App() {
return (
<ExplodingInput
placeholder="Type to celebrate..."
triggerMode="keypress"
particlePreset="confetti"
/>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
triggerMode | "keypress" | "submit" | "focus" | "clear" | "custom" | "keypress" | Modes that trigger the explosion |
particlePreset | "confetti" | "sparks" | "stars" | "bubbles" | "letters" | "emoji" | "confetti" | Type of spawned particles |
customEmoji | string[] | DEFAULT_EMOJI | Array of emoji characters |
characterParticles | boolean | false | If true, the typed character itself becomes the explosion particle |
direction | "up" | "down" | "left" | "right" | "radial" | "burst" | "up" | Direction of the particle spread |
cursorTrail | boolean | false | Trailing spark effect continuously while focused |
validate | (value: string) => boolean | undefined | Changes particle color to green/red based on validity |
maxParticles | number | 400 | Max canvas particles alive at once |
audio | "pop" | "whoosh" | "sparkle" | string | undefined | Triggers a sound effect hook |
explodeRef | React.Ref<ExplodingInputHandle> | undefined | Ref used to manually trigger the explode() instance method |