Docs
Glimmer Input

Glimmer Input

A magical input field that generates sparkling particles as you type.

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.**

PropTypeDescription
classNamestringOptional custom classes for the input element
containerClassNamestringOptional 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>