Option Picker
A grid (or row) of selectable option cards — quiz answers, plan tiers, settings options, survey questions. Each option carries a label, an optional description, and an optional icon. Single-select uses radiogroup semantics with arrow-key navigation; multi-select wraps native checkboxes.
Customize
Shape
single
grid
2
Installation
npx shadcn@latest add https://craftbits.dev/r/option-picker.jsonUsage
import { OptionPicker } from "@craft-bits/core";
const [plan, setPlan] = useState("pro");
<OptionPicker
options={[
{ value: "starter", label: "Starter", description: "1 project." },
{ value: "pro", label: "Pro", description: "Unlimited projects." },
{ value: "team", label: "Team", description: "Shared workspaces." },
]}
value={plan}
onValueChange={setPlan}
columns={3}
aria-label="Select a plan"
/>Multi-select — set mode="multiple". value / defaultValue / onValueChange switch to string[]:
const [channels, setChannels] = useState<string[]>(["email"]);
<OptionPicker
mode="multiple"
options={[
{ value: "email", label: "Email" },
{ value: "sms", label: "SMS" },
{ value: "push", label: "Push" },
]}
value={channels}
onValueChange={setChannels}
aria-label="Notification channels"
/>Understanding the component
- Single vs multiple.
mode="single"(default) rendersrole="radiogroup"withrole="radio"buttons and roving tabindex.mode="multiple"wraps a real<input type="checkbox">in a<label>per option — native form semantics work out of the box. - Layout.
orientation="grid"(default) drivesgrid-template-columns: repeat(columns, …).'horizontal'is a single flex row;'vertical'is a stacked column. - Card anatomy. Optional icon badge (left), label + optional description (middle), selection indicator (right). The indicator is a radio dot or check mark depending on
mode. - Selection styling. Selected: accent border + faint accent-muted tint + subtle shadow. Hover: lifts to a muted background. Focus-visible: 2-px accent ring offset against
--cb-bg. Tap: canonicalTAP_SCALEwithSPRINGS.snap. - Keyboard models. Single-mode: Arrow Left/Up + Right/Down step through enabled options (wrap-around), Home / End jump to ends, each arrow press also selects. Multi-mode: Tab + Space — each option is its own checkbox so no special handling is needed.
- Disabled state.
disabledon an individual option dims the card and removes it from the keyboard nav order.disabledon the root dims the group and disables every option.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | OptionPickerOption[] | required | Options to render. Order is preserved. |
mode | 'single' | 'multiple' | 'single' | Selection model. 'multiple' switches value / defaultValue / onValueChange to string[]. |
value | string | string[] | — | Controlled selection. Pair with onValueChange. |
defaultValue | string | string[] | — | Uncontrolled initial selection. |
onValueChange | (value: string) => void or (value: string[]) => void | — | Fired when the selection changes. Signature matches mode. |
orientation | 'horizontal' | 'vertical' | 'grid' | 'grid' | Layout direction. |
columns | number | 2 | Number of grid columns when orientation="grid". |
disabled | boolean | false | Disables every option in the group. |
aria-label | string | — | Accessible name for the group. |
className | string | — | Merged onto the rendered root <div>. |
OptionPickerOption
| Field | Type | Description |
|---|---|---|
value | string | Stable identifier for this option. |
label | ReactNode | Headline label shown in the card. |
description | ReactNode | Optional secondary text below the label. |
icon | ReactNode | Optional leading icon. |
disabled | boolean | Disable this single option. |
Accessibility
- Single-mode renders
<div role="radiogroup">with each option as a<button role="radio">.aria-checked,aria-disabled, anddata-state="checked" | "unchecked"reflect the selection. - Multi-mode renders
<div role="group">and wraps a real<input type="checkbox">in a<label>per option. The native input is visually hidden viasr-onlybut remains focusable. - Always pass
aria-label(oraria-labelledby) on the root so the group is announced. - Focus is visible via a 2-px ring keyed to
--cb-accentwith--cb-bgas the ring-offset. - Color contrast: selected cards use
--cb-fgon--cb-accent-muted; inactive cards use--cb-fgon--cb-bg— both pass WCAG AA in the default theme. - Tap-scale and colour transitions respect
prefers-reduced-motion— Motion collapseswhileTapto an instant swap, and the colour transition is short enough to be imperceptible at reduced-motion speeds.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/interaction/OptionPicker.tsx). The original was a quiz answer picker; craft-bits generalizes it into a typed, accessible single/multi-select grid.