bigbullui logobigbullui
Componentpickers

Variant Picker

Color swatch and size variant picker with animated selection.

Preview

Live Preview
Premium Headphones
25%

Premium Headphones

$299$399

Jordan Daisy

Product Designer

AvailableRemote
The Future of Design Systems

The Future of Design Systems

Alex Rivera8 min read
Oc
t 14

Tech Conference 2026

21:00
Berlin Arena
Invoice #INV-2026-0987draft
Date: 2026-09-15Due: 2026-10-15
ItemQtyPrice
Design Services1$150
Development Hours5$100
SubtotalNaN
Tax (10%)NaN
TotalNaN
THERMAL RECEIPT#D9DZLH
Design Consultation$150
Development Hours$200
Total$385.00
Thank you for your purchase!
Premium Headphones
1
Phone Case
2
Subtotal$377.00
Tax$30.16
Total$407.16
Color
Size
Order #102938Processing
Design Services1x
Development5x
VIP

LOYALTY TIER

$100

Jordan Daisy

Happy Birthday!

J

Jordan Daisy

Product Designer

UX

Badge: EMP-8942

Wedding Reception

Please confirm your attendance

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

Usage

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

<VariantPicker />

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-amber-500 hover:bg-amber-400",
};

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
sizesstring[]Size options list.
valuestringControlled selection.
onColorChange(name: string) => voidColor swatch callback.
onSizeChange(size: string) => voidSize selection callback.