Skip to content
Tier

Choice card group

Pro

A choice between a few options that each need more than a word: a plan, a model, a way to run something. Each option is a card with a title, a description and, if it helps, a detail such as a price or a limit; the group is one radio group, so arrow keys move between the cards and choose (following the reading direction), and it is one tab stop.

Nothing is chosen until the person chooses: leave out defaultValue, and pass value={null} when you control it, because a card already chosen nudges people to accept it without reading the others. A card that can't be chosen takes locked with the reason, and stays in the arrow-key order so the reason can be read.

The cards sit in up to maxAcross columns, as many as the group's own width allows (a container query, so a group in a narrow panel shows one column whatever the window's width). Name the group with label or aria-labelledby.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0 and 4.2.0: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/choice-card-group

Brings in: locale, locked, tokens, utils · npm: @base-ui/react@^1.9.0, lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { FileText, MessageSquareMore, Send } from "lucide-react"
import { ChoiceCard, ChoiceCardGroup } from "@/components/candor/ui/choice-card-group"
import { TooltipProvider } from "@/components/candor/ui/tooltip"

// Nothing is chosen until the person chooses: no defaultValue, and null while the app holds the value. A card already
// chosen would nudge people to accept it without reading the others. A card that can't be chosen says why.
export default function Example() {
  const [mode, setMode] = React.useState<string | null>(null)
  return (
    <TooltipProvider>
      <div className="flex w-full max-w-3xl flex-col gap-3">
        <ChoiceCardGroup label="How should the agent reply to new tickets?" value={mode} onValueChange={setMode}>
          <ChoiceCard value="draft" icon={FileText} title="Draft only" description="It writes a reply and leaves it for you to send." />
          <ChoiceCard
            value="ask"
            icon={MessageSquareMore}
            title="Draft and ask"
            description="It writes a reply and asks you before sending it."
            detail="Up to 50 tickets a day"
          />
          <ChoiceCard
            value="send"
            icon={Send}
            title="Send on its own"
            description="It replies without asking."
            locked="Your workspace's policy requires a person to approve replies"
          />
        </ChoiceCardGroup>
        <a href="#plans" className="text-sm text-accent-text underline-offset-4 hover:underline">
          Compare what each mode can do
        </a>
      </div>
    </TooltipProvider>
  )
}

<ChoiceCardGroup>

PropTypeDefaultDescription
valuestring | null | undefined—The chosen card's value, when the app controls it; `null` while nothing is chosen.
defaultValuestring | undefined—The card chosen at first, when the group keeps its own state. Leave it out so nothing is chosen for the person.
onValueChange((value: string) => void) | undefined—Called with the value of the card the person chooses.
labelstring | undefined—The group's name, shown above the cards and read as the radio group's name. Give this or `aria-labelledby`.
"aria-labelledby"string | undefined—The id of an element elsewhere that names the group, in place of `label`.
maxAcross1 | 2 | 3 | 4 | undefined3The most cards side by side, when the group is wide enough. Defaults to 3.
namestring | undefined—The form field's name: the chosen card's value is submitted under it.
requiredboolean | undefined—A card must be chosen before the form is submitted.
classNamestring | undefined—Classes for the group's outer box.
childrenrequiredReact.ReactNode—The `ChoiceCard`s.

<ChoiceCard>

One option in a ChoiceCardGroup: a card that is a single radio. Its name is its title; its description and detail describe it. Everything on the card is read as part of that one radio, so put nothing that can be pressed inside it: a link to compare plans goes beside the group.

A chosen card has a thicker border and a check mark, not a colour alone, and both are drawn as borders and strokes, so they stay visible in forced colours.

PropTypeDefaultDescription
valuerequiredstring—The value the group reports while this card is chosen.
titlerequiredstring—The option's name, shown at the top of the card and read as the radio's name.
descriptionReact.ReactNode | undefined—What choosing it means, in a sentence or two. Read after the name.
detailReact.ReactNode | undefined—A line under the description, such as a price, a limit or a figure. Text and figures only: it is part of the radio, so nothing in it can be pressed. Read after the description.
iconLucideIcon | undefined—An icon beside the title, drawn for the eye only.
lockedstring | null | undefined—Why this card can't be chosen right now. It stays in the arrow-key order, is marked `aria-disabled` and says why (see `Locked`).
classNamestring | undefined—Classes, merged with Candor's.

Motion

MomentFamilyWhat it does
tinttintA card's border changes colour when the person chooses it, and a ring fades in when it takes keyboard focus. A card already chosen when the group appears is drawn chosen at once.

Set with --cd-motion-choice-card-group-<moment>-* in your brand file; the families are in the design reference.