Installation
Copy and paste the following code into your project.
components/ui/glimmer-input.tsx
"use client";
import React, { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
interface PixieDustInputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
className?: string;
containerClassName?: string;
}
const MAX_LIFE = 45; // Reduced lifetime for quicker fade
const COLORS = [
{ r: 255, g: 255, b: 255, a: 0.8 }, // Pure white
{ r: 255, g: 250, b: 215, a: 0.7 }, // Warm gold
{ r: 255, g: 248, b: 220, a: 0.7 }, // Soft gold
{ r: 253, g: 245, b: 230, a: 0.7 }, // Old lace
{ r: 255, g: 253, b: 245, a: 0.7 }, // Ivory
];
export function GlimmerInput({
className,
containerClassName,
...props
}: PixieDustInputProps) {
const inputRef = useRef<HTMLInputElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const animationFrameRef = useRef<number | undefined>(undefined);
useEffect(() => {
const canvas = canvasRef.current;
const input = inputRef.current;
if (!canvas || !input) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const resizeCanvas = () => {
if (!canvas) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
const particles: any[] = [];
let lastTime = performance.now();
let hue = 0; // For rainbow effect on some particles
function burst(intensity: number) {
if (!input || !ctx || !canvas) return;
const field = input.getBoundingClientRect();
const behavior = [
// Refined movement with gentle drift
(p: any, deltaTime: number) => {
const forceFactor = 0.992;
const windStrength = Math.sin(performance.now() / 3000) * 0.01;
p.velocity.x +=
(-0.008 * forceFactor * deltaTime) / 16 + windStrength;
p.velocity.y += (-0.008 * forceFactor * deltaTime) / 16;
// Subtle random movement
const time = performance.now() / 2000;
const noiseX =
Math.sin(time * p.uniqueOffset + p.position.x / 100) * 0.01;
const noiseY =
Math.cos(time * p.uniqueOffset + p.position.y / 100) * 0.01;
p.velocity.x += (noiseX * deltaTime) / 16;
p.velocity.y += (noiseY * deltaTime) / 16;
// Gentle spiral motion
const age = p.life / MAX_LIFE;
const spiral = Math.sin(age * Math.PI * 2) * 0.08 * (1 - age);
p.velocity.x +=
(spiral * Math.cos(age * Math.PI * 2) * deltaTime) / 16;
p.velocity.y +=
(spiral * Math.sin(age * Math.PI * 2) * deltaTime) / 16;
},
// Enhanced particle interaction with attraction/repulsion
(p: any) => {
particles.forEach((neighbor) => {
if (neighbor === p) return;
const dx = neighbor.position.x - p.position.x;
const dy = neighbor.position.y - p.position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 70) {
const force = Math.min(0.0004, 1 / (distance * 6));
const repel = distance < 30 ? -1 : 1;
p.velocity.x += dx * force * repel * 0.5;
p.velocity.y += dy * force * repel * 0.5;
}
});
},
// Enhanced movement with subtle trails
(p: any, deltaTime: number) => {
const ease = 0.98;
p.velocity.x *= ease;
p.velocity.y *= ease;
p.position.x += (p.velocity.x * deltaTime) / 16;
p.position.y += (p.velocity.y * deltaTime) / 16;
// Refined trail system
if (Math.random() < 0.3 && p.life < MAX_LIFE * 0.7) {
p.trail.push({
x: p.position.x,
y: p.position.y,
age: 0,
size: p.size * (0.2 + Math.random() * 0.2),
opacity: 0.05 + Math.random() * 0.15,
});
}
// Update trail with gentle fade
for (let i = p.trail.length - 1; i >= 0; i--) {
p.trail[i].age++;
if (p.trail[i].age > 8) {
p.trail.splice(i, 1);
}
}
},
];
const baseSize = 1.2; // Smaller base size
const force = 0.6; // Gentler force
const progress =
Math.min(field.width, input.value.length * 10) / field.width;
const offset = field.left + field.width * progress;
const rangeMin = Math.max(field.left, offset - 40);
const rangeMax = Math.min(field.right, offset + 20);
// Generate more varied particles
const particleCount = Math.floor(intensity * 1.8);
for (let i = 0; i < particleCount; i++) {
const angle = (Math.PI * 2 * i) / particleCount;
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
const isSpecial = Math.random() < 0.15; // Special particles with rainbow effect
particles.push({
position: {
x: rangeMin + 10 + Math.random() * (rangeMax - rangeMin - 20),
y: field.top + 15 + Math.random() * (field.height - 30),
},
velocity: {
x: (Math.random() - 0.5) * force * 1.5,
y: (Math.random() - 0.5) * force * 1.5,
},
size: baseSize * (0.8 + Math.random() * 0.4),
life: 0,
behavior,
trail: [],
color,
isSpecial,
uniqueOffset: Math.random() * 10,
initialOpacity: 0.35 + Math.random() * 0.45,
rotationSpeed: (Math.random() - 0.5) * 0.25,
rotation: Math.random() * Math.PI * 2,
pulsePhase: Math.random() * Math.PI * 2,
sparkleRate: 0.7 + Math.random() * 0.3,
});
}
if (input.value.length === 1) {
for (let i = 0; i < intensity * 2; i++) {
const angle = (Math.PI * 2 * i) / (intensity * 2);
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
const isSpecial = Math.random() < 0.15; // Special particles with rainbow effect
particles.push({
position: {
x: field.left + Math.cos(angle) * 20,
y:
field.top +
field.height / 2 +
(Math.sin(angle) * field.height) / 3,
},
velocity: {
x: Math.cos(angle) * force * 0.8,
y: Math.sin(angle) * force * 0.8,
},
size: baseSize * (0.8 + Math.random() * 0.4),
life: 0,
behavior,
trail: [],
color,
isSpecial,
uniqueOffset: Math.random() * 10,
initialOpacity: 0.35 + Math.random() * 0.45,
rotationSpeed: (Math.random() - 0.5) * 0.25,
rotation: Math.random() * Math.PI * 2,
pulsePhase: Math.random() * Math.PI * 2,
sparkleRate: 0.7 + Math.random() * 0.3,
});
}
}
if (rangeMax === field.right) {
for (let i = 0; i < intensity * 2; i++) {
const angle = (Math.PI * i) / intensity;
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
const isSpecial = Math.random() < 0.15; // Special particles with rainbow effect
particles.push({
position: {
x: field.right,
y:
field.top +
field.height / 2 +
(Math.sin(angle) * field.height) / 3,
},
velocity: {
x: Math.cos(angle) * force * 0.8,
y: Math.sin(angle) * force * 0.8,
},
size: baseSize * (0.8 + Math.random() * 0.4),
life: 0,
behavior,
trail: [],
color,
isSpecial,
uniqueOffset: Math.random() * 10,
initialOpacity: 0.35 + Math.random() * 0.45,
rotationSpeed: (Math.random() - 0.5) * 0.25,
rotation: Math.random() * Math.PI * 2,
pulsePhase: Math.random() * Math.PI * 2,
sparkleRate: 0.7 + Math.random() * 0.3,
});
}
}
}
function animate() {
if (!ctx || !canvas) return;
const currentTime = performance.now();
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life++;
if (p.life > MAX_LIFE) {
particles.splice(i, 1);
continue;
}
p.behavior.forEach((b: Function) => b(p, deltaTime));
p.rotation += p.rotationSpeed * (deltaTime / 16);
// Smooth fade calculation
const progress = p.life / MAX_LIFE;
const easeProgress = 1 - Math.pow(1 - progress, 3);
let opacity = Math.sin(Math.PI * (1 - easeProgress)) * p.initialOpacity;
// Subtle pulse effect
const pulseSpeed = 0.003;
const pulseAmount = 0.1;
opacity *=
1 + Math.sin(currentTime * pulseSpeed + p.pulsePhase) * pulseAmount;
// Draw refined trails
p.trail.forEach((t: any, index: number) => {
const trailAge = t.age / 8;
const trailOpacity = opacity * (1 - trailAge) * t.opacity;
// Subtle trail glow
const glowSize = t.size * 2;
const glowGradient = ctx.createRadialGradient(
t.x,
t.y,
0,
t.x,
t.y,
glowSize,
);
glowGradient.addColorStop(
0,
`rgba(${p.color.r},${p.color.g},${p.color.b},${trailOpacity * 0.4})`,
);
glowGradient.addColorStop(
1,
`rgba(${p.color.r},${p.color.g},${p.color.b},0)`,
);
ctx.beginPath();
ctx.arc(t.x, t.y, glowSize, 0, Math.PI * 2);
ctx.fillStyle = glowGradient;
ctx.fill();
});
// Main particle with elegant glow
ctx.save();
ctx.translate(p.position.x, p.position.y);
ctx.rotate(p.rotation);
// Refined glow effect
const glowRadius = p.size * 2.8;
const mainGlow = ctx.createRadialGradient(0, 0, 0, 0, 0, glowRadius);
mainGlow.addColorStop(
0,
`rgba(${p.color.r},${p.color.g},${p.color.b},${opacity * 0.8})`,
);
mainGlow.addColorStop(
0.5,
`rgba(${p.color.r},${p.color.g},${p.color.b},${opacity * 0.3})`,
);
mainGlow.addColorStop(
1,
`rgba(${p.color.r},${p.color.g},${p.color.b},0)`,
);
ctx.beginPath();
ctx.arc(0, 0, glowRadius, 0, Math.PI * 2);
ctx.fillStyle = mainGlow;
ctx.fill();
// Core particle with subtle shine
if (Math.random() < p.sparkleRate * 0.7) {
const sparkleSize = p.size * (0.6 + Math.random() * 0.3);
ctx.beginPath();
ctx.arc(0, 0, sparkleSize, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${p.color.r},${p.color.g},${p.color.b},${opacity})`;
ctx.fill();
}
// Elegant star shape
const rayCount = 4;
const innerRadius = p.size * 0.3;
const outerRadius = p.size * 1.8;
ctx.beginPath();
for (let j = 0; j < rayCount; j++) {
const angle = (j * Math.PI * 2) / rayCount - p.rotation;
const rayLength =
outerRadius *
(0.95 + Math.sin(currentTime * 0.008 + p.pulsePhase + j) * 0.05);
ctx.lineTo(Math.cos(angle) * rayLength, Math.sin(angle) * rayLength);
ctx.lineTo(
Math.cos(angle + Math.PI / rayCount) * innerRadius,
Math.sin(angle + Math.PI / rayCount) * innerRadius,
);
}
ctx.closePath();
ctx.fillStyle = `rgba(${p.color.r},${p.color.g},${p.color.b},${opacity * 0.3})`;
ctx.fill();
ctx.restore();
}
animationFrameRef.current = requestAnimationFrame(animate);
}
animate();
const handleKeyup = (e: KeyboardEvent) => {
if (!input) return;
const keys = [
8, 9, 13, 16, 17, 18, 27, 32, 33, 34, 35, 36, 37, 38, 39, 40, 46, 91,
93, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123,
];
if (keys.indexOf(e.keyCode) === -1) {
burst(12);
input.classList.add("keyup");
setTimeout(() => input.classList.remove("keyup"), 100);
}
};
input.addEventListener("keyup", handleKeyup);
return () => {
window.removeEventListener("resize", resizeCanvas);
input.removeEventListener("keyup", handleKeyup);
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, []);
return (
<div className={cn("relative w-full", containerClassName)}>
<div className="group/input relative isolate w-full overflow-hidden rounded-lg">
<div className="absolute inset-0 rounded-lg bg-linear-to-br from-transparent via-black/[0.01] to-black/[0.02] opacity-0 transition-opacity duration-500 group-hover/input:opacity-100 dark:via-white/[0.01] dark:to-white/[0.02]" />
<input
ref={inputRef}
type="text"
className={cn(
"relative w-full",
"rounded-lg border",
// Enhanced background with subtle gradient
"bg-linear-to-b from-white/95 to-white/90",
"dark:bg-linear-to-b dark:from-black/90 dark:to-black/85",
"backdrop-blur-[2px] dark:backdrop-blur-[4px]",
// Refined border
"border-black/[0.07] dark:border-white/[0.07]",
"px-4 py-3 text-base",
"text-zinc-900 dark:text-zinc-100",
"placeholder:text-zinc-500 dark:placeholder:text-zinc-400",
// Enhanced shadow with subtle shine
"shadow-[0_2px_3px_-2px_rgba(0,0,0,0.05),inset_0_1px_0_0_rgba(255,255,255,0.5)]",
"dark:shadow-[0_2px_3px_-2px_rgba(255,255,255,0.05),inset_0_1px_0_0_rgba(255,255,255,0.05)]",
"transition-all duration-200",
// Enhanced hover state
"hover:border-black/[0.12] hover:bg-linear-to-b",
"hover:from-white hover:to-white/95",
"dark:hover:border-white/[0.12]",
"dark:hover:from-black/95 dark:hover:to-black/90",
"hover:shadow-[0_3px_6px_-2px_rgba(0,0,0,0.08),inset_0_1px_0_0_rgba(255,255,255,0.6)]",
"dark:hover:shadow-[0_3px_6px_-2px_rgba(255,255,255,0.08),inset_0_1px_0_0_rgba(255,255,255,0.07)]",
// Enhanced focus state
"focus:border-black/[0.15]",
"focus:to-white/98 focus:bg-linear-to-b focus:from-white",
"dark:focus:border-white/[0.15]",
"dark:focus:from-black/98 dark:focus:to-black/95",
"focus:outline-hidden focus:ring-[3px]",
"focus:ring-black/[0.06] dark:focus:ring-white/[0.06]",
"focus:shadow-[0_4px_7px_-2px_rgba(0,0,0,0.12),inset_0_1px_0_0_rgba(255,255,255,0.7)]",
"dark:focus:shadow-[0_4px_7px_-2px_rgba(255,255,255,0.12),inset_0_1px_0_0_rgba(255,255,255,0.1)]",
className,
)}
{...props}
/>
</div>
<canvas
ref={canvasRef}
className="pointer-events-none fixed inset-0 z-50"
/>
</div>
);
}
Update the import paths to match your project setup:
import GlimmerInput from "@/components/ui/glimmer-input";
Usage
import { GlimmerInput } from "@/components/ui/glimmer-input";
export default function Example() {
return (
<GlimmerInput
placeholder="Type something..."
onChange={(e) => console.log("Value:", e.target.value)}
/>
);
}
Props
** The component extends the native HTML input element props with additional styling options.**
Prop | Type | Description |
---|---|---|
className | string | Optional custom classes for the input element |
containerClassName | string | Optional custom classes for the input container |
All standard HTML input attributes are also supported (type, placeholder, value, onChange, etc.).
Examples
Basic Input
<GlimmerInput placeholder="Enter your name" />
With Custom Styling
<GlimmerInput
className="w-full max-w-md"
containerClassName="my-4"
placeholder="Search..."
/>
Form Integration
<form onSubmit={(e) => e.preventDefault()}>
<GlimmerInput required type="email" placeholder="Enter your email" />
<button type="submit">Submit</button>
</form>