Try snips - A smooth code presentation tool

Color Pickers

An animated color picker where selected swatches burst into particles and fluidly morph into the preview panel inspired by Kumail Nanji.

Loading demo...

default.tsx
"use client";
 
import { motion, AnimatePresence } from "motion/react";
import { useState, useRef } from "react";
 
const COLORS = [
  "var(--color-red-500)",
  "var(--color-blue-500)",
  "var(--color-green-500)",
  "var(--color-yellow-500)",
  "var(--color-pink-500)",
  "var(--color-orange-500)",
];
 
interface Particle {
  id: number;
  color: string;
  spawnX: number;
  spawnY: number;
  targetX: number;
  targetY: number;
  targetWidth: number;
  targetHeight: number;
}
 
const ColorPicker = () => {
  const [previewIndex, setPreviewIndex] = useState(0);
  const [activeIndex, setActiveIndex] = useState(0);
  const [particles, setParticles] = useState<Particle[]>([]);
  const previewRef = useRef<HTMLDivElement>(null);
 
  const handleColorClick = (
    index: number,
    e: React.MouseEvent<HTMLButtonElement>,
  ) => {
    setActiveIndex(index);
 
    const buttonRect = e.currentTarget.getBoundingClientRect();
    const previewRect = previewRef.current?.getBoundingClientRect();
    const containerRect = previewRef.current
      ?.closest(".relative")
      ?.getBoundingClientRect();
 
    if (!previewRect || !containerRect) return;
 
    const spawnX = buttonRect.left + buttonRect.width / 2 - containerRect.left;
    const spawnY = buttonRect.top + buttonRect.height / 2 - containerRect.top;
    const targetX =
      previewRect.left + previewRect.width / 2 - containerRect.left;
    const targetY =
      previewRect.top + previewRect.height / 2 - containerRect.top;
 
    const newParticle: Particle = {
      id: Math.random(),
      color: COLORS[index],
      spawnX,
      spawnY,
      targetX,
      targetY,
      targetWidth: previewRect.width,
      targetHeight: previewRect.height,
    };
 
    setParticles((prev) => [...prev, newParticle]);
 
    setTimeout(() => {
      setPreviewIndex(index);
      setTimeout(() => {
        setParticles((prev) => prev.filter((p) => p.id !== newParticle.id));
      }, 500);
    }, 500);
  };
 
  return (
    <div className="size-full flex items-center justify-center overflow-hidden text-white">
      <div className="flex flex-col items-center gap-10 relative mb-20 rounded-2xl">
        <div className="relative ">
          <motion.div
            ref={previewRef}
            animate={{
              scale: [0.95, 1],
              opacity: [0.9, 1],
              backgroundColor: COLORS[previewIndex],
            }}
            className="w-100 h-50 rounded-2xl  relative z-10"
            transition={{
              scale: { type: "spring", stiffness: 400, damping: 25 },
              opacity: { duration: 0.2 },
              backgroundColor: { delay: 0.5, duration: 0 },
            }}
          />
        </div>
 
        <AnimatePresence>
          {particles.map((particle) => {
            const particleSize = 12;
 
            return (
              <motion.div
                key={particle.id}
                initial={{
                  x: particle.spawnX - particleSize / 2,
                  y: particle.spawnY - particleSize / 2,
                  width: particleSize,
                  height: particleSize,
                  borderRadius: particleSize / 2,
                  opacity: 1,
                }}
                animate={{
                  x: [
                    particle.spawnX - particleSize / 2,
                    particle.targetX - particleSize / 2,
                    particle.targetX - particle.targetWidth / 2,
                  ],
                  y: [
                    particle.spawnY - particleSize / 2,
                    particle.targetY - particleSize / 2,
                    particle.targetY - particle.targetHeight / 2,
                  ],
                  width: [particleSize, particleSize, particle.targetWidth],
                  height: [particleSize, particleSize, particle.targetHeight],
                  borderRadius: [
                    particleSize / 2,
                    particleSize / 2,
                    "calc(var(--radius) + 8px)",
                  ],
                  opacity: [1, 1, 1],
                }}
                transition={{
                  duration: 0.5,
                  times: [0, 0.5, 1],
                  ease: ["easeOut", "linear"],
                }}
                className="absolute top-0 left-0 z-50 pointer-events-none"
                style={{ backgroundColor: particle.color }}
              />
            );
          })}
        </AnimatePresence>
      </div>
      <div className="flex gap-4 rounded-2xl absolute bottom-5 isolate">
        {COLORS.map((color, index) => (
          <button
            key={index}
            className="size-10 cursor-pointer relative"
            onClick={(e) => handleColorClick(index, e)}
          >
            <div
              style={{ backgroundColor: color }}
              className="size-full absolute inset-0 rounded z-20"
            />
            {activeIndex === index && (
              <motion.div
                layoutId="active-color"
                transition={{
                  duration: 0.5,
                  type: "spring",
                  stiffness: 400,
                  damping: 50,
                }}
                className="size-12.5 border-2 rounded-md absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none -z-10"
              />
            )}
          </button>
        ))}
      </div>
    </div>
  );
};
 
export default ColorPicker;