Componentpickers1.14 KB gzipZero dependencies

Color Picker

Palette swatch picker with framing and active mark.

Preview

Live Preview
TICKET PALETTE
#BC3A28
ACTIVE TICKET INK: #BC3A28

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add color-picker

Or install the full npm package:

npm install bigbullui
import { ColorPicker } from "@/components/ui/color-picker";

<ColorPicker
  defaultValue="#BC3A28"
  onValueChange={(c) => console.log(c)}
/>

Usage guidance

Use Color Picker when a ticket stub surface needs Palette swatch picker with framing and active mark. Keep props minimal and prefer composition with sibling stubs.

Avoid Color Picker for long form flows or dense data grids where a dedicated layout block fits better.

Source code

color-picker.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface ColorPickerProps {
  value?: string;
  defaultValue?: string;
  onValueChange?: (color: string) => void;
  colors?: string[];
  className?: string;
}

const DEFAULT_STUB_COLORS = [
  "#BC3A28", // Stamp Red
  "#17130C", // Ink Black
  "#F6F0E0", // Warm Cream
  "#ECE3CC", // Sand
  "#D8C9AC", // Tan
  "#16120B", // Dark Night
  "#E0573D", // Bright Stamp
  "#4A3F2C", // Dark Input
];

export function ColorPicker({
  value: controlledValue,
  defaultValue = "#BC3A28",
  onValueChange,
  colors = DEFAULT_STUB_COLORS,
  className,
}: ColorPickerProps) {
  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
  const isControlled = controlledValue !== undefined;
  const currentVal = isControlled ? controlledValue : uncontrolledValue;

  const handleSelect = (color: string) => {
    if (!isControlled) {
      setUncontrolledValue(color);
    }
    onValueChange?.(color);
  };

  return (
    <div
      className={cn(
        "inline-flex flex-col gap-3 rounded-lg border-2 border-dashed border-border bg-card p-3 shadow-sm select-none",
        className
      )}
    >
      <div className="flex items-center justify-between border-b border-dashed border-border pb-2">
        <span className="font-mono text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground">
          TICKET PALETTE
        </span>
        <div className="flex items-center gap-1.5">
          <span
            style={{ backgroundColor: currentVal }}
            className="size-3.5 rounded-xs border border-foreground/30 shadow-xs"
          />
          <span className="font-mono text-xs font-semibold text-foreground">
            {currentVal.toUpperCase()}
          </span>
        </div>
      </div>

      <div className="grid grid-cols-4 gap-2">
        {colors.map((c) => {
          const isSelected = c.toLowerCase() === currentVal.toLowerCase();
          return (
            <button
              key={c}
              type="button"
              onClick={() => handleSelect(c)}
              style={{ backgroundColor: c }}
              aria-label={`Select color ${c}`}
              className={cn(
                "relative size-8 rounded-sm border border-foreground/20 shadow-xs transition-transform hover:scale-110 cursor-pointer",
                "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
                isSelected && "ring-2 ring-accent ring-offset-1 scale-105"
              )}
            >
              {isSelected ? (
                <span className="absolute inset-0 flex items-center justify-center font-mono text-xs font-bold text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.8)]">
                  ✓
                </span>
              ) : null}
            </button>
          );
        })}
      </div>
    </div>
  );
}

Props

PropTypeDescription
value?string
defaultValue?string
onValueChange?(color: string) => void
colors?string[]
className?string