Choice Card Group
A data-driven set of selectable cards — radio (single) or checkbox (multi) — each a bordered card with a title, helper line, and an optional leading icon, with built-in label / description / error and native form submission.
ChoiceCardGroup is the richer cousin of RadioGroup and Checkbox: instead of a bare control beside a label, each option is a bordered card with a title, an optional helper line, and an optional leading icon or avatar. Selecting a card tints its border and fill with the active palette. One prop — multiple — switches between single-choice (radio) and multi-choice (checkbox). It implements the kit's value-based form-control contract, so name submits and onChange hands you the value directly; label, description, and error are built in via Field.
import { ChoiceCardGroup } from '@42/ui-react/choice-card-group';
<ChoiceCardGroup
label="Next project"
defaultValue="minishell"
items={[
{ value: 'philosophers', label: 'Philosophers', description: 'Threads and mutexes.' },
{ value: 'minishell', label: 'Minishell', description: 'Parsing, pipes, builtins.' },
]}
/>Playground
Component
Dir
Presets
Props
Code
<Component orientation="vertical" size="md" label="Next project"/>Single vs. multi
By default the group is single-choice: it rides Ark UI's RadioGroup, the value is a string | null, and each card shows a radio dot. Pass multiple to make it multi-choice: it rides CheckboxGroup, the value becomes a string[], and each card shows a checkbox tick. onChange hands you the next value directly in both modes.
Code
// single — value is a string | null<ChoiceCardGrouplabel="Next project"defaultValue="minishell"items={[ { value: 'philosophers', label: 'Philosophers', description: 'Threads and mutexes.' }, { value: 'minishell', label: 'Minishell', description: 'Parsing, pipes, builtins.' }, { value: 'cub3d', label: 'Cub3D', description: 'Raycasting, Wolfenstein-style.' },]}/>// multi — value is a string[]<ChoiceCardGroupmultiplelabel="Notifications"defaultValue={['email']}items={[ { value: 'email', label: 'Email', description: 'Evaluation summaries in your inbox.' }, { value: 'sms', label: 'SMS', description: 'Texts when a correction slot opens up.' }, { value: 'push', label: 'Push', description: 'Cluster and defense reminders.' },]}/>Items
items accepts bare strings (where the value is the label) or { value, label, description?, icon?, disabled? } objects — mixed freely.
// 1 — strings (value === label)
<ChoiceCardGroup items={['Common Core', 'Piscine']} />
// 2 — objects, with optional per-item description + disabled
<ChoiceCardGroup
items={[
{ value: 'morning', label: 'Morning', description: 'Before the cluster fills up.' },
{ value: 'afternoon', label: 'Afternoon', description: 'Peak cluster hours.' },
{ value: 'evening', label: 'Evening', disabled: true },
]}
/>Leading icon
Give an item an icon — any node, so an icon or an avatar — and that card lays it at the start and pushes the selection indicator to the end. Cards without an icon keep the indicator at the start.
Code
import { Cpu, Terminal, Box } from 'lucide-react';<ChoiceCardGrouplabel="Next project"defaultValue="minishell"items={[ { value: 'philosophers', label: 'Philosophers', description: 'Threads and mutexes.', icon: <Cpu /> }, { value: 'minishell', label: 'Minishell', description: 'Parsing, pipes, builtins.', icon: <Terminal /> }, { value: 'cub3d', label: 'Cub3D', description: 'Raycasting, Wolfenstein-style.', icon: <Box /> },]}/>Variants
The indicator (the tick / dot) reuses the shared Checkbox surface, so it has the same two variants: default is a monochrome mark; filled paints the checked mark with the active color. The card's selected border + soft fill always follow the palette.
Code
<ChoiceCardGroup variant="default" color="brand" defaultValue="push-swap" items={[...]} /><ChoiceCardGroup variant="filled" color="brand" defaultValue="push-swap" items={[...]} />Sizes
Five sizes — xs through xl — matching the rest of the family. The card padding, text, icon, and indicator all scale together.
Code
<ChoiceCardGroup size="sm" items={['Common Core', 'Piscine']} defaultValue="Common Core" /><ChoiceCardGroup size="md" items={['Common Core', 'Piscine']} defaultValue="Common Core" /><ChoiceCardGroup size="lg" items={['Common Core', 'Piscine']} defaultValue="Common Core" />Colors
The selected card's border + soft fill, and the filled indicator, ride color through the --c-* slots — set it once and every kit palette tints for free.
Code
<ChoiceCardGroup color="brand" variant="filled" defaultValue="a" items={[...]} /><ChoiceCardGroup color="green" variant="filled" defaultValue="a" items={[...]} /><ChoiceCardGroup color="purple" variant="filled" defaultValue="a" items={[...]} />Custom rendering
By default each card renders its label and description. Pass renderItem to fill the text block yourself — it receives the (normalized) item and the live { checked, disabled } state, and keeps the card's surface, icon slot, indicator, and selection wiring. Define it in a "use client" module so it survives the server → client boundary.
'use client';
<ChoiceCardGroup
multiple
items={bonuses}
renderItem={(item, { checked }) => (
<span className="flex items-center justify-between gap-2">
<span className="font-medium">{item.label}</span>
<span className={checked ? 'text-(--c-text)' : 'text-gray-light-500'}>{item.description}</span>
</span>
)}
/>Label, description & error
ChoiceCardGroup wraps itself in Field, so label / description / error / required work out of the box and wire the aria-* relationships. Passing error flips the group to the invalid (red) state — independently settable via invalid for form libraries.
Controlled
Drive the value from state. onChange hands you the selected value directly — a string | null (single) or a string[] (multi).
'use client';
import { useState } from 'react';
import { ChoiceCardGroup } from '@42/ui-react/choice-card-group';
export function BonusPicker() {
const [bonuses, setBonuses] = useState<string[]>([]);
return (
<ChoiceCardGroup
multiple
label="Bonuses"
value={bonuses}
onChange={setBonuses}
items={['Bonus part', 'Extra flags', 'Unit tests']}
/>
);
}Forms
Each card is backed by a hidden native input sharing the field name — a radio per card in single mode, a checkbox per card in multi mode — so the selection rides a native <form> submission and FormData, no controlled state required.
<form action={save}>
<ChoiceCardGroup multiple name="bonuses" items={['Bonus part', 'Extra flags']} />
</form>API
Prop
Type
Accessibility
- Built on Ark UI's
RadioGroup(single) /CheckboxGroup(multi) machines: each card is a clickable<label>wrapping a hidden native input, so the whole card toggles and exposes the right role (radio/checkbox) andaria-checked. - In single mode,
←/→/↑/↓move and select; selection follows focus. In multi mode,Tabmoves between cards andSpacetoggles. The keyboard focus ring is drawn on the whole card. invalid(orerror) flips the group to the red state; label / description / error associations are wired via the built-inField.- A leading
iconis decorative (aria-hidden); the card'slabelcarries the accessible name. - RTL is honored via the document
dir— the icon, text, and indicator mirror automatically (logicalstart/end).
Segment Group
A segmented single-choice control on Ark UI's SegmentGroup machine — a compact, always-visible row of mutually exclusive segments with a sliding pill, built-in label / description / error, and native form submission.
Combobox
The kit's composable search-select primitive — a thin styled layer over Ark UI's Combobox. The engine Autocomplete and MultiSelect build on, and the escape hatch for fully custom pickers.