<!-- Protocore Design System — CardInput -->
# CardInput

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/card-input`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { CardInput } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/card-input

> Selectable option cards for plan / tier / layout pickers — radio single-choice or checkbox multi-choice, selection inverting to an accent ring.

## When to use it

Use **CardInput** when each option carries enough content — a title, a description, a price — that a plain radio or checkbox row would feel cramped, and seeing the options side by side aids the choice. For terse, equal-weight options use **RadioGroup** / **CheckboxGroup**; for a compact view-mode toggle use **SegmentedControl**.

## Props

### CardInput.Group

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `aria-label` | `string` | no | — | Accessible name for the group. |
| `className` | `string` | no | — | — |
| `defaultValue` | `string \| string[]` | no | — | Initial selection when uncontrolled. |
| `disabled` | `boolean` | no | — | Disable every card in the group. |
| `name` | `string` | no | — | Shared `name` for the underlying inputs — required for radio grouping in single mode. |
| `onValueChange` | `((value: string \| string[]) => void)` | no | — | Fires with the next selection: a string in single mode, string[] in multiple. |
| `orientation` | `enum` | no | `horizontal` | Layout of the cards. Default `"horizontal"` (wrapping row). |
| `style` | `CSSProperties` | no | — | — |
| `type` | `enum` | no | `single` | `"single"` = radio semantics (one value), `"multiple"` = checkbox (array). Default `"single"`. |
| `value` | `string \| string[]` | no | — | Controlled selection — a string (single) or string[] (multiple). |

### CardInput.Option

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `checked` | `boolean` | no | — | Controlled checked state when used standalone (no Group). |
| `children` | `ReactNode` | no | — | Extra content below the description. |
| `className` | `string` | no | — | — |
| `defaultChecked` | `boolean` | no | — | Initial checked state when used standalone and uncontrolled. |
| `description` | `ReactNode` | no | — | Secondary line under the title (muted). |
| `disabled` | `boolean` | no | — | Disable just this card. |
| `icon` | `ReactNode` | no | — | Optional leading icon slot. |
| `onCheckedChange` | `((checked: boolean) => void)` | no | — | Fires with the next checked state when used standalone. |
| `style` | `CSSProperties` | no | — | — |
| `title` | `ReactNode` | no | — | Primary label (sans, ink). |
| `value` | `string` | no | — | This option's value — required inside a `CardInput.Group`. |

## Examples

### Single choice

Wrap `CardInput.Option`s in a `CardInput.Group`. The default `type="single"` gives radio semantics — one value, arrow-key navigation — and the selected card takes the accent ring.

```tsx
import { CardInput } from "@protocore/pds";

export default function Demo() {
  return (
    <CardInput.Group defaultValue="team" aria-label="Plan" name="plan">
      <CardInput.Option value="hobby" title="Hobby" description="1 project · community support" />
      <CardInput.Option value="team" title="Team" description="Unlimited projects · 99.9% SLA" />
      <CardInput.Option
        value="enterprise"
        title="Enterprise"
        description="SSO · 99.99% SLA · priority support"
      />
    </CardInput.Group>
  );
}
```

### Multiple choice

Set `type="multiple"` for checkbox semantics: the group owns a `string[]` and each card toggles independently.

```tsx
import { useState } from "react";
import { CardInput } from "@protocore/pds";

export default function Demo() {
  const [value, setValue] = useState<string | string[]>(["ci"]);

  return (
    <CardInput.Group
      type="multiple"
      value={value}
      onValueChange={setValue}
      aria-label="Add-ons"
    >
      <CardInput.Option value="ci" title="CI minutes" description="+2,000 build minutes / mo" />
      <CardInput.Option value="seats" title="Extra seats" description="Add teammates beyond 5" />
      <CardInput.Option value="backups" title="Daily backups" description="30-day retention" />
    </CardInput.Group>
  );
}
```

## Do & don't

**Do**

- Give the group an accessible name via `aria-label` or a wrapping `Field`.
- Keep each card's title and description parallel across options.
- Set a `name` on the group when the value participates in a native form.

**Don't**

- Don't nest other interactive controls inside a card — the whole card is the control.
- Don't use single-choice cards where more than one can apply — set `type="multiple"`.
- Don't rely on color alone; the selected card also shows a checked marker.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Tab` | Move focus into the group (single: the selected card). |
| `Arrow keys` | Move and select between cards in single mode. |
| `Space` | Toggle the focused card in multiple mode. |

**Notes**

- Each card is a `<label>` wrapping a real radio/checkbox, so selection is native and screen-reader-correct.
- The group is a `role="radiogroup"` (single) or `role="group"` (multiple).
- Focus draws the standard accent ring on the card surface via the hidden input's `:focus-visible`.

## Related

`radio-group`, `checkbox-group`, `segmented-control`, `card`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
