Componentpickers1.11 KB gzipZero dependencies

Variant Picker

Color swatch and size variant picker with animated selection.

Preview

Live Preview
Color
Size

Color: accent / Size: M

Installation

Tokens guide →

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

npx bigbullui add variant-picker

Or install the full npm package:

npm install bigbullui
import { VariantPicker } from "@/components/ui/variant-picker";

<VariantPicker />

Usage guidance

Use Variant Picker when a ticket stub surface needs Color swatch and size variant picker with animated selection. Keep props minimal and prefer composition with sibling stubs.

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

Source code

variant-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 ColorOption {
  name: string;
  className?: string;
}

export interface VariantPickerProps {
  colors: ColorOption[];
  sizes?: string[];
  value?: string;
  defaultValue?: string;
  onColorChange?: (name: string) => void;
  onSizeChange?: (size: string) => void;
  className?: string;
}

const ColorSwatch = "color-swatch";
const SelectedSwatch = "color-swatch-selected";
const SizeChip = "size-picker-chip";
const FadeIn = "variant-picker-fade-in";

// Explicit color map — NO dynamic class strings
const colorMap: Record<string, string> = {
  navy: "bg-navy hover:bg-navy/80",
  green: "bg-green-500 hover:bg-green-400",
  black: "bg-gray-900 hover:bg-gray-800",
  white: "bg-white hover:bg-gray-100",
  brown: "bg-warning hover:bg-warning",
};

export function VariantPicker({
  colors,
  sizes,
  value,
  defaultValue,
  onColorChange,
  onSizeChange,
  className,
}: VariantPickerProps) {
  const [controlledValue] = React.useState<string | undefined>(
    defaultValue ?? undefined
  );

  return (
    <div className={cn("space-y-3", className)}>
      {/* Color swatches */}
      <div>
        <span className="text-xs uppercase tracking-wider text-muted-foreground mb-1">Color</span>
        <div className="flex gap-1.5">
          {colors.map((opt, i) => {
            const selected = controlledValue === opt.name;
            const baseClass = colorMap[opt.name] || "bg-gray-200 hover:bg-gray-300";

            return (
              <button
                key={i}
                type="button"
                className={cn(
                  "rounded-full w-6 h-6 transition-colors",
                  ColorSwatch,
                  selected && SelectedSwatch,
                  "motion-reduce:transition-none",
                  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                )}
                style={{ backgroundColor: opt.className ? `var(${opt.className})` : "var(--secondary)" }}
                onClick={() => onColorChange?.(opt.name)}
                aria-label={`Select ${opt.name} color`}
              >
                {/* Swatch fills from explicit map */}
              </button>
            );
          })}
        </div>
      </div>

      {/* Size row */}
      {sizes && sizes.length > 0 && (
        <div>
          <span className="text-xs uppercase tracking-wider text-muted-foreground mb-1">Size</span>
          <div className="flex gap-1.5">
            {sizes.map((size, i) => (
              <button
                key={i}
                type="button"
                className={cn(
                  "inline-flex rounded-md border border-2 border-dashed items-center justify-center gap-1.5 px-3 py-1.5 text-xs font-mono uppercase tracking-wider",
                  SizeChip,
                  "motion-reduce:transition-none",
                  "cursor-pointer"
                )}
                onClick={() => onSizeChange?.(size)}
                aria-label={`Select size ${size}`}
              >
                {size}
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

Props

PropTypeDescription
colorsColorOption[]
sizes?string[]
value?string
defaultValue?string
onColorChange?(name: string) => void
onSizeChange?(size: string) => void
className?string