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.json

Usage

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

  1. Single vs multiple. mode="single" (default) renders role="radiogroup" with role="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.
  2. Layout. orientation="grid" (default) drives grid-template-columns: repeat(columns, …). 'horizontal' is a single flex row; 'vertical' is a stacked column.
  3. 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.
  4. 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: canonical TAP_SCALE with SPRINGS.snap.
  5. 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.
  6. Disabled state. disabled on an individual option dims the card and removes it from the keyboard nav order. disabled on the root dims the group and disables every option.

Props

PropTypeDefaultDescription
optionsOptionPickerOption[]requiredOptions to render. Order is preserved.
mode'single' | 'multiple''single'Selection model. 'multiple' switches value / defaultValue / onValueChange to string[].
valuestring | string[]Controlled selection. Pair with onValueChange.
defaultValuestring | string[]Uncontrolled initial selection.
onValueChange(value: string) => void or (value: string[]) => voidFired when the selection changes. Signature matches mode.
orientation'horizontal' | 'vertical' | 'grid''grid'Layout direction.
columnsnumber2Number of grid columns when orientation="grid".
disabledbooleanfalseDisables every option in the group.
aria-labelstringAccessible name for the group.
classNamestringMerged onto the rendered root <div>.

OptionPickerOption

FieldTypeDescription
valuestringStable identifier for this option.
labelReactNodeHeadline label shown in the card.
descriptionReactNodeOptional secondary text below the label.
iconReactNodeOptional leading icon.
disabledbooleanDisable this single option.

Accessibility

  • Single-mode renders <div role="radiogroup"> with each option as a <button role="radio">. aria-checked, aria-disabled, and data-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 via sr-only but remains focusable.
  • Always pass aria-label (or aria-labelledby) on the root so the group is announced.
  • Focus is visible via a 2-px ring keyed to --cb-accent with --cb-bg as the ring-offset.
  • Color contrast: selected cards use --cb-fg on --cb-accent-muted; inactive cards use --cb-fg on --cb-bg — both pass WCAG AA in the default theme.
  • Tap-scale and colour transitions respect prefers-reduced-motion — Motion collapses whileTap to 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.