shadimage
Spell UI
componentcolor-selectorsource: ok · hit

Color Selector

Interactive color picker component.

npx shadcn add "https://spell.sh/r/color-selector.json"
free

Interaction

dropdown

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Files1 · source cached 10/15/2026, 7:29:31 AM

registry/spell-ui/color-selector.tsx
"use client";

import { useState } from "react";
import { cn } from "@/lib/utils";

interface ColorSelectorProps {
  colors: string[];
  size?: "default" | "sm" | "lg";
  defaultValue: string;
  name?: string;
  onColorSelect?: (color: string) => void;
  className?: string;
}

const colorMap = {
  default: "var(--foreground)",
  red: "var(--color-red-500)",
  green: "var(--color-green-500)",
  blue: "var(--color-blue-500)",
  yellow: "var(--color-yellow-500)",
  purple: "var(--color-purple-500)",
  pink: "var(--color-pink-500)",
  indigo: "var(--color-indigo-500)",
  orange: "var(--color-orange-500)",
  teal: "var(--color-teal-500)",
  cyan: "var(--color-cyan-500)",
  lime: "var(--color-lime-500)",
  emerald: "var(--color-emerald-500)",
  violet: "var(--color-violet-500)",
  fuchsia: "var(--color-fuchsia-500)",
  rose: "var(--color-rose-500)",
  sky: "var(--color-sky-500)",
  amber: "var(--color-amber-500)",
} as const;

export function ColorSelector(
  { colors, size = "default", defaultValue, name, onColorSelect, className }:
    ColorSelectorProps,
) {
  const [selectedColor, setSelectedColor] = useState<string>(defaultValue);

  const handleColorSelect = (color: string) => {
    setSelectedColor(color);
    onColorSelect?.(color);
  };

  const getSizeClass = (size: "default" | "sm" | "lg") => {
    switch (size) {
      case "sm":
        return "size-4";
      case "default":
        return "size-5";
      case "lg":
        return "size-6";
      default:
        return "size-5";
    }
  };

  const getColorValue = (color: string): string => {
    return colorMap[color as keyof typeof colorMap] || color;
  };

  const sizeClass = getSizeClass(size);

  return (
    <div className={cn("flex gap-2", className)}>
      {name && (
        <input
          type="hidden"
          name={name}
          value={selectedColor}
        />
      )}
      {colors.map((color) => {
        const colorValue = getColorValue(color);
        return (
          <div
            key={color}
            className={`${sizeClass} rounded-full cursor-pointer transition-transform duration-200 active:scale-90 ${
              selectedColor === color
                ? "ring-2 ring-offset-2 ring-gray-400"
                : ""
            }`}
            style={{
              backgroundColor: colorValue,
              ...(selectedColor === color && {
                boxShadow:
                  `inset 0 0 0 2px var(--background), 0 0 0 2px ${colorValue}`,
              }),
            }}
            onClick={() => handleColorSelect(color)}
            onKeyDown={(e) => {
              if (e.key === "Enter" || e.key === " ") {
                e.preventDefault();
                handleColorSelect(color);
              }
            }}
            tabIndex={0}
            role="button"
            aria-label={`Select ${color} color`}
            aria-pressed={selectedColor === color}
          />
        );
      })}
    </div>
  );
}

More from Spell UI

Browse all components in this library →