Inputs
Swatch Picker
An accessible grid of preset color swatches — radiogroup semantics, roving tabindex, and a contrast-safe selected check mark.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/swatch-picker.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "SwatchPicker" component (lucide-react Check).
Contract
- Export a forwardRef div extending HTMLAttributes (omit native onChange).
- Props: swatches ({ value: string; label: string }[]), value (string | null,
controlled), onChange ((value: string) => void), size ("sm" | "md", default
"md"), disabled (default false), label (aria name, default "Color").
- swatch.value is any valid CSS color and is consumer data — it is painted
verbatim via inline style and is not subject to the component's own
semantic-token rule.
Behavior
- Container is role="radiogroup"; each swatch is a role="radio" button with
aria-checked and aria-label = swatch.label.
- Roving tabindex: only one swatch is ever tabbable — the selected one, or
the first swatch when nothing is selected (value has no match).
- Keyboard: ArrowRight/ArrowDown move to the next swatch, ArrowLeft/ArrowUp
to the previous, both circular (wrap past the last/first); Home/End jump
to the first/last. Moving both focuses and selects (calls onChange) —
same semantics as a native radio group.
- Click selects a swatch directly and moves focus to it.
- Selected swatch: ring-2 ring-offset-2 ring-ring, plus a centered Check icon.
- disabled disables every button (no focus, no click) and dims the group.
Rendering & styling
- Semantic tokens only for the component's own chrome: border-border/50 on
each swatch, ring-ring + ring-offset-background on the selected one,
focus-visible ring tokens. The only intentional exception is the check
mark: since it sits on an arbitrary consumer-supplied background, no
token can guarantee contrast, so it renders as literal white with a dark
drop-shadow for definition — documented, not accidental.
- Hover scale (hover:scale-110) is disabled under prefers-reduced-motion
(transition and the scale itself). Merge className via cn().
Customization levers
- Shape: swap rounded-full for rounded-md if the product wants square chips.
- Selection: add a multi-select variant (array value + toggle) for tagging
use cases — out of scope here, this component is single-select.
- Validation: if swatches come from user input rather than a fixed design
set, validate value as a real CSS color before rendering.
- Sizes: extend the SIZES/CHECK_SIZES maps for an "lg" tier.Concepts
- Roving tabindex — the radiogroup exposes exactly one tab stop; arrow keys move it instead of Tab, matching native radio-group behavior.
- Consumer-supplied color data —
swatch.valueis any CSS color the caller provides (hex, a named color,var(--chart-1)); it paints the swatch directly and sits outside this component's own semantic-token rule, which still governs every other pixel (border, ring, focus outline). - Contrast-safe overlay — the selected check mark can't rely on a theme token because it must stay legible against whatever color the caller passes in, including a swatch that's already white. It uses literal white plus a drop-shadow instead.
- Preset palette selection — this is for choosing among a small, designer-defined set of colors, not for free-form color entry.