<!-- Generated by scripts/gen-docs.mts from the JSDoc in registry/. Do not edit; run `pnpm run docs`. -->

# ChoiceCard

A set of choices shown as cards, each with a picture, a label and a line of description, for picking exactly one: a cover photo, a plan, a layout. The cards are a native radio group under a legend, so the arrow keys move between them, a plain form submits the chosen value, and screen readers hear "Cover photo, radio group" and each card's label and description. The chosen card takes the primary color and a check.

```tsx
import { ChoiceCard } from "@/components/ouro/choice-card/choice-card";
```

## Example

```tsx
<ChoiceCard
  legend="Cover photo"
  name="cover"
  items={photos.map((photo) => ({ value: photo.id, label: photo.title, media: <img src={photo.url} alt="" /> }))}
/>
```

## Use instead when

- Use `RadioGroup` instead for plain text choices, `SegmentedControl` for two to five short options that switch a view, and `Select` for a long list.

## Props

Type: `ChoiceCardProps`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` |  | Layout only: margins, width (`mt-6`). Appearance classes warn in development. |
| `columns` | `2 \| 3 \| 4` | `3` | How many cards a row holds on a wide screen; on a phone each row holds one, or two with `columns` of 3 or more. |
| `defaultValue` | `string` |  | The value chosen at first, for an uncontrolled group. |
| `description` | `ReactNode` |  | Supporting text under the legend, read with the group. |
| `disabled` | `boolean` |  | Disables every card. |
| `error` | `ReadonlyErrorMessages` |  | What is wrong, shown under the cards in the destructive tone and read with each choice. |
| `hideLegend` | `boolean` |  | Keeps the legend for screen readers only, when a heading above already says it. |
| `items` (required) | `readonly ChoiceCardItem[]` |  | The choices. |
| `legend` (required) | `string` |  | What is being chosen, above the cards and read as the group's name. |
| `name` | `string` |  | The form field name the chosen value submits under. |
| `onValueChange` | `((value: string) => void)` |  | Called with the value of the card the person chooses. |
| `ref` | `Ref<HTMLFieldSetElement>` |  | Reaches the rendered element. |
| `required` | `boolean` |  | A choice must be made before the form submits; the legend shows the required mark. |
| `value` | `string` |  | The chosen value, for a controlled group; pair it with `onValueChange`. |

## Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

- `label`: Not a prop. The group's name is `legend`: `<ChoiceCard legend="Cover photo" />`.
- `onChange`: Not a prop. Ouro UI says `onValueChange`: `<ChoiceCard onValueChange={setCover} />`.
- `options`: Not a prop. The choices are `items`: `<ChoiceCard items={[{ value, label, media }]} />`.

## `className` is for layout only

Pass margins, width and grid placement (`mt-4`, `w-full`, `col-span-2`). Your classes always win over Ouro UI's: ChoiceCard merges `className` last with `cn` (tailwind-merge), so a class that conflicts with its own replaces it.[^layout-classes]

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (`hover:`, `sm:`) and any `!` are removed, it starts with `bg-`, `text-`, `border-`, `rounded-`, `shadow-`, `font-`, `ring-`, `from-`, `via-`, `to-`, `fill-` or `stroke-`, or is `border`, `rounded`, `shadow` or `ring`, or has a `dark:` variant. Text wrapping, overflow and table layout classes (`text-wrap`, `text-ellipsis`, `border-collapse`) are layout and do not warn.

Alignment classes (`text-center`) warn too: set alignment on a parent element.

## Types

### `ChoiceCardItem`

One card in a `ChoiceCard`'s `items`.

```ts
interface ChoiceCardItem {
  /** What the group submits when this card is chosen, and its `value` then. */
  value: string;
  /** The choice's name, under its media. */
  label: string;
  /** Supporting text under the label, read after it. */
  description?: ReactNode | undefined;
  /**
   * A picture of the choice above its label, such as `<img src={cover} alt="" />`: give it
   * `alt=""` when the label says what it shows. It fills the card's width.
   */
  media?: ReactNode | undefined;
  /** Disables this one card; the arrow keys skip it. */
  disabled?: boolean | undefined;
}
```

### `ErrorMessage`

One error message: text, or a React element for a message that carries a link or other markup.

```ts
type ErrorMessage = string | ReactElement;
```

### `ReadonlyErrorMessages`

A read-only message view accepted by fields and Form; mutable input arrays remain valid.

```ts
type ReadonlyErrorMessages = ErrorMessage | readonly ErrorMessage[];
```

[^layout-classes]: test:registry/ouro/components/ouro/choice-card/choice-card.test.tsx::ChoiceCard > forwards a ref and lets layout classes win, with no margin of its own verify:testExists(registry/ouro/components/ouro/choice-card/choice-card.test.tsx::ChoiceCard > forwards a ref and lets layout classes win, with no margin of its own)
