# Selection Card

> Options as cards: a dot for one answer, a check for several, with a hint and an aside like a price on each card.

- Name: `selection-card`
- Status: beta
- Categories: forms
- File: the install writes `selection-card.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `@base-ui/react`, `class-variance-authority`. The install adds them.
- Gallery: https://arbokui.com/components/selection-card
- Registry item: https://arbokui.com/r/selection-card.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/selection-card
   ```

## Use it for

- A question whose options each need a hint, a price, or a consequence, like a plan, a delivery speed, or which teams to tell.
- One answer by default. Add multiple for several answers, and give value and defaultValue a string array.
- Put the option's name in the children, the explanation in hint, and a short figure like a price in aside.
- Name the question with a small field label above the cards and point aria-labelledby at it. Use aria-label when there is no visible label.
- Lay the cards out with className on SelectionGroup, like className='grid-cols-2'.
- Set tone and size once, on SelectionGroup. Every card follows them.

## Avoid it for

- Short, plain options with nothing to explain, in a dense form. Use Radio Group or Checkbox.
- A setting that turns on or off. Use a switch.
- Long lists, more than about six options. Use a select or a combobox.
- A heading above the cards. A small field label names the question.
- Starting with nothing chosen when one answer is the safe default. Set defaultValue to it.

## Props

- `SelectionGroup`: `SelectionGroupProps`. Root. Holds the value. Takes value, defaultValue, onValueChange, disabled, and, for one answer, name, required, and readOnly.
- `multiple`: `boolean`. Default `false`. SelectionGroup. Several answers. Cards draw a check, and value is a string array.
- `aria-label`: `string`. SelectionGroup. Names the question. TypeScript requires this or aria-labelledby.
- `aria-labelledby`: `string`. SelectionGroup. The id of the visible field label above the cards.
- `tone`: `"neutral" | "brand" | "danger" | "success" | "warning"`. Default `"neutral"`. SelectionGroup. Color of a chosen card's edge and control.
- `size`: `"sm" | "md" | "lg"`. Default `"md"`. SelectionGroup. Text and control size. Density scales them.
- `value`: `string | string[]`. SelectionGroup: the chosen answer, or answers with multiple. SelectionCard: what the group holds when this card is chosen. Required on a card.
- `defaultValue`: `string | string[]`. SelectionGroup. The answer, or answers, chosen at the start when you do not control it.
- `onValueChange`: `(value, details) => void`. SelectionGroup. Called with the new value, a string, or a string array with multiple.
- `aria-invalid`: `boolean`. SelectionGroup. Turns every card's edge to the danger tone. Set it when validation fails, and link the message with aria-describedby.
- `SelectionCard`: `ComponentProps<"label">`. Part. One answer. The whole card is the target.
- `children`: `ReactNode`. SelectionCard. The option's name. It names the control.
- `hint`: `ReactNode`. SelectionCard. A second line that explains the option. Read as the control's description.
- `aside`: `ReactNode`. SelectionCard. Short text at the end of the card, like a price. Read as part of the description.
- `disabled`: `boolean`. Default `false`. SelectionGroup or SelectionCard. Blocks choosing. On the group it blocks every card.
- `className`: `string`. SelectionGroup: classes for the layout. SelectionCard: classes for the card. Later classes win.

## Accessibility

- One answer renders role="radiogroup" with a role="radio" in each card. Several answers render a group with a role="checkbox" in each card. Each also has a hidden native input, so the value is sent with a form.
- The title names the control through aria-labelledby. The hint and the aside describe it through aria-describedby.
- With one answer the group is one tab stop, and arrow keys move the answer, skipping disabled cards. With several, each card is a tab stop and Space toggles it.
- The control takes focus, and the whole card shows the 2px focus ring. The theme tests hold --ring to 3:1 on the page and cards.
- A resting card's edge uses --input, which the theme tests hold to 3:1. A chosen card adds the tone's edge and an inset ring, so the choice does not rest on color alone: the dot or the check shows it too.
- TypeScript requires a name for the group and a value for each card.

## Examples

### One answer, named by a field label

```tsx
<div className="grid gap-1.5">
  <p id="plan-label" className="text-sm text-muted-foreground">
    Plan
  </p>
  <SelectionGroup aria-labelledby="plan-label" name="plan" defaultValue="team" tone="brand">
    <SelectionCard value="solo" hint="One person, three projects." aside="£0">
      Solo
    </SelectionCard>
    <SelectionCard value="team" hint="Up to ten people, every project." aside="£12 a month">
      Team
    </SelectionCard>
  </SelectionGroup>
</div>
```

### Several answers, in two columns

```tsx
<SelectionGroup multiple aria-label="Who to tell" defaultValue={["design"]} className="grid-cols-2">
  <SelectionCard value="design" hint="19 people get a message.">
    Design team
  </SelectionCard>
  <SelectionCard value="managers" hint="12 managers get a copy.">
    Managers
  </SelectionCard>
</SelectionGroup>
```

### Controlled

```tsx
"use client";
import { useState } from "react";

function Delivery() {
  const [speed, setSpeed] = useState("standard");
  return (
    <SelectionGroup aria-label="Delivery speed" value={speed} onValueChange={(value) => setSpeed(value)}>
      <SelectionCard value="standard" hint="3 to 5 days." aside="Free">
        Standard
      </SelectionCard>
      <SelectionCard value="express" hint="Next day, if you order by 14:00." aside="£6">
        Express
      </SelectionCard>
    </SelectionGroup>
  );
}
```
