# Choice card

One choice drawn as a card, with a label and a line under it: picked like a radio, or ticked like a checkbox.

Block: `smbk-choice-card`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `mark` | `radio`, `checkbox` | `radio` | A radio when one card of a group is picked; a checkbox when several can be. |
| `error` | on / off | off | The choice is invalid, such as a required question left empty. The card takes the danger edge, and its mark its error look. |

Parts: `smbk-choice-card__text`, `smbk-choice-card__label`, `smbk-choice-card__description`

## Markup

```html
<label class="smbk-choice-card smbk-choice-card--mark-radio smbk-radio smbk-radio--size-md">
  <input
    type="radio"
    class="smbk-radio__control"
    aria-labelledby="choicechoice-card-1-label"
    aria-describedby="choicechoice-card-1-description"
    name="cc-configure"
    checked=""
    value="standard"
  >
  <span class="smbk-radio__circle" aria-hidden="true">
    <span class="smbk-radio__dot"></span>
  </span>
  <span class="smbk-choice-card__text">
    <span id="choicechoice-card-1-label" class="smbk-choice-card__label">Standard delivery</span>
    <span
      id="choicechoice-card-1-description"
      class="smbk-choice-card__description"
    >Three to five working days, tracked.</span>
  </span>
</label>
<label class="smbk-choice-card smbk-choice-card--mark-radio smbk-radio smbk-radio--size-md">
  <input
    type="radio"
    class="smbk-radio__control"
    aria-labelledby="choicechoice-card-2-label"
    aria-describedby="choicechoice-card-2-description"
    name="cc-configure"
    value="express"
  >
  <span class="smbk-radio__circle" aria-hidden="true">
    <span class="smbk-radio__dot"></span>
  </span>
  <span class="smbk-choice-card__text">
    <span id="choicechoice-card-2-label" class="smbk-choice-card__label">Express delivery</span>
    <span
      id="choicechoice-card-2-description"
      class="smbk-choice-card__description"
    >Next working day, before noon.</span>
  </span>
</label>
```

## React

```tsx
import { ChoiceCard } from './design-system/components/choice-card/choice-card.js';

<ChoiceCard mark="radio" name="cc-configure" value="standard" description="Three to five working days, tracked." defaultChecked>Standard delivery</ChoiceCard>
<ChoiceCard mark="radio" name="cc-configure" value="express" description="Next working day, before noon.">Express delivery</ChoiceCard>
```

## Stylesheet

```css
/* smbk-choice-card — Choice card. Figma "Choice card" 7105:1911 (State: Default, Hover, Pressed, Focus, Disabled, Error × Selected).
 * One choice drawn as a card: the radio's circle or the checkbox's box, then a
 * label in Label MD over an optional line in Body SM, the whole card the
 * label of one native radio or checkbox. Built on the radio and the checkbox:
 * the card carries their block and size classes, so the mark's own looks
 * (hover, chosen, error, disabled) are theirs, and this stylesheet adds only
 * the card: its surface, its edge, its padding and the focus ring around it.
 * The label over its line, and the mark on the label's first line, are the
 * rules every choice shares, in choice/choice.css.
 * Chosen, the edge turns the brand color at the emphasis width, drawn inside
 * so the card keeps its size, and the mark shows the choice as a shape too. */

/* Two classes, so the card wins over the radio's and the checkbox's own row whichever stylesheet loads first. */
.smbk-choice-card:is(.smbk-radio, .smbk-checkbox) {
  box-sizing: border-box;
  display: flex;
  align-items: flex-start;
  gap: var(--smbk-space-inline-md);
  width: 100%;
  padding: calc(var(--smbk-space-inset-xl) - var(--smbk-border-width-hairline));
  border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-rim);
  border-radius: var(--smbk-radius-container-md);
  background: var(--smbk-color-surface-default);
  box-shadow: var(--smbk-elevation-control);
  color: var(--smbk-color-text-default);
  font-family: var(--smbk-typography-label-md-font-family);
  font-size: var(--smbk-typography-label-md-font-size);
  font-weight: var(--smbk-typography-label-md-font-weight);
  line-height: var(--smbk-typography-label-md-line-height);
  letter-spacing: var(--smbk-typography-label-md-letter-spacing);
}

@media (hover: hover) {
  .smbk-choice-card:is(.smbk-radio, .smbk-checkbox):hover:has(:enabled) {
    background: var(--smbk-color-surface-raised);
  }
}

/* Pressed: the card sinks onto the sunken surface and drops its lift. */
.smbk-choice-card:is(.smbk-radio, .smbk-checkbox):active:has(:enabled) {
  background: var(--smbk-color-surface-sunken);
  box-shadow: none;
}

/* Chosen: the brand edge at the emphasis width, the hairline border plus the rest drawn inside. */
.smbk-choice-card:is(.smbk-radio, .smbk-checkbox):has(:checked) {
  border-color: var(--smbk-color-border-brand);
  box-shadow: inset 0 0 0 calc(var(--smbk-border-width-emphasis) - var(--smbk-border-width-hairline)) var(--smbk-color-border-brand), var(--smbk-elevation-control);
}

/* Pressed while chosen keeps the brand edge, not the lift; in error the danger edge below stays. */
.smbk-choice-card:not(.smbk-choice-card--error, :has([aria-invalid="true"])):is(.smbk-radio, .smbk-checkbox):active:has(:enabled:checked) {
  box-shadow: inset 0 0 0 calc(var(--smbk-border-width-emphasis) - var(--smbk-border-width-hairline)) var(--smbk-color-border-brand);
}

/* Error, chosen or not: the danger edge at the emphasis width; its control marked aria-invalid="true" draws the same. */
.smbk-choice-card.smbk-choice-card--error:is(.smbk-radio, .smbk-checkbox):has(:enabled),
.smbk-choice-card:is(.smbk-radio, .smbk-checkbox):has(:enabled[aria-invalid="true"]) {
  border-color: var(--smbk-color-feedback-danger-border);
  box-shadow: inset 0 0 0 calc(var(--smbk-border-width-emphasis) - var(--smbk-border-width-hairline)) var(--smbk-color-feedback-danger-border), var(--smbk-elevation-control);
}

/* The focus ring goes around the whole card, not the mark. */
.smbk-choice-card:is(.smbk-radio, .smbk-checkbox):has(:focus-visible) {
  outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
  outline-offset: var(--smbk-focus-ring-offset);
}

.smbk-choice-card :is(.smbk-radio__control, .smbk-checkbox__control):focus-visible + :is(.smbk-radio__circle, .smbk-checkbox__box) {
  outline: none;
}

.smbk-choice-card .smbk-radio__control:not(:checked):focus-visible + .smbk-radio__circle {
  background: transparent;
}

/* Disabled: the disabled surface with no lift; chosen, the edge keeps its width in the disabled border color. */
.smbk-choice-card:is(.smbk-radio, .smbk-checkbox):has(:disabled) {
  background: var(--smbk-color-surface-disabled);
  box-shadow: none;
}

.smbk-choice-card:is(.smbk-radio, .smbk-checkbox):has(:disabled:checked) {
  border-color: var(--smbk-color-action-disabled-border);
  box-shadow: inset 0 0 0 calc(var(--smbk-border-width-emphasis) - var(--smbk-border-width-hairline)) var(--smbk-color-action-disabled-border);
}

/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops shadows, so the chosen card's edge is the border alone:
   Highlight, as the chosen mark is. Nothing here applies outside that mode. */

@media (forced-colors: active) {
  .smbk-choice-card:is(.smbk-radio, .smbk-checkbox):has(:checked) {
    border-color: Highlight;
  }

  .smbk-choice-card:is(.smbk-radio, .smbk-checkbox):has(:disabled) {
    border-color: GrayText;
  }
}
```

## Tokens it reads

```json
{
  "space": {
    "text": {
      "label": {
        "$type": "dimension",
        "$value": "{{space.0}}",
        "$description": "Between a label or title and the line of supporting text right under it, inside a component. None: untrimmed line height already spaces the two lines."
      }
    },
    "inline": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things side by side, medium."
      }
    },
    "inset": {
      "xl": {
        "$type": "dimension",
        "$value": "{{space.16}}",
        "$description": "Padding inside a component, extra large."
      }
    }
  },
  "size": {
    "selection": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.20}}",
        "$description": "Checkbox and radio box, medium."
      }
    }
  },
  "color": {
    "text": {
      "subtlest": {
        "$type": "color",
        "$value": "{{color.grey.600}}",
        "$description": "Quietest text: hints, captions, placeholders."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.500}}",
        "$description": "Text in unavailable controls."
      },
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      }
    },
    "border": {
      "rim": {
        "$type": "color",
        "$value": "{{color.alpha.white.90}}",
        "$description": "The light rim that makes a surface look lit: fields, chosen pills, cards."
      },
      "brand": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Brand-red edge; also the logo’s mark."
      },
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      }
    },
    "surface": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.100}}",
        "$description": "Standard surface: cards, panels, the header."
      },
      "raised": {
        "$type": "color",
        "$value": "{{color.grey.50}}",
        "$description": "One floor above default: popovers, raised panels."
      },
      "sunken": {
        "$type": "color",
        "$value": "{{color.alpha.grey-800.5}}",
        "$description": "Recessed wells: fields, tracks, code. See-through, so a well always shows one step below what it sits on: the page, a card or a panel."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.200}}",
        "$description": "Surface of a disabled card or field."
      }
    },
    "feedback": {
      "danger": {
        "border": {
          "$type": "color",
          "$value": "{{color.red.500}}",
          "$description": "Danger messages: edge; the edge of a field in error (3:1)."
        }
      }
    },
    "action": {
      "disabled": {
        "border": {
          "$type": "color",
          "$value": "{{color.grey.300}}",
          "$description": "The edge of every disabled action, shared by every kind of button."
        }
      }
    }
  },
  "typography": {
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    },
    "label-md": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 500,
        "lineHeight": 1.4,
        "letterSpacing": "0px"
      },
      "$description": "Label MD: labels on controls and short UI text (buttons, chips, tabs, menu headings). Geist Medium, sentence case. Code and keys use Code SM and Code MD."
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    },
    "emphasis": {
      "$type": "dimension",
      "$value": "{{border-width.2}}",
      "$description": "Two-pixel edges for emphasis: a chosen card, a focus ring."
    }
  },
  "radius": {
    "container": {
      "md": {
        "$type": "dimension",
        "$value": "{{radius.12}}",
        "$description": "Corners of cards, panels and dialogs, medium."
      }
    }
  },
  "elevation": {
    "control": {
      "$type": "shadow",
      "$value": {
        "light": "0px 0px 1px 0px #373d430d, 0px 1px 2px 0px #373d431a, 0px 2px 4px 0px #373d430d",
        "dark": "0px 0px 1px 0px #0000001a, 0px 1px 2px 0px #00000026, 0px 2px 4px 0px #0000001a"
      }
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      },
      "offset": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Gap between a control and its focus ring."
      }
    }
  }
}
```

## Purpose

One choice drawn as a card: a title, a line under it, and the radio's circle or the checkbox's box. The whole card is the label of one real radio or checkbox, so it is picked by a press anywhere on it. It gives a few choices that each need explaining more room and a larger target than a radio row.

## Anatomy

- The block, the card: a label element holding everything, on the default surface with a hairline edge and a lift. It also carries the radio's or the checkbox's classes, which draw the mark.
- The mark, the radio's circle or the checkbox's box, on the title's first line. It always shows, so being picked is never told by color alone.
- The text part, the title over the line.
- The label part, the title.
- The description part, an optional line under the title.

## When to use

- Two to five choices that each need a line of explanation: a plan, a delivery speed, a look.
- With the checkbox mark, a few add-ons that can each be picked.

## When not to use

- Many choices, or short ones. Use [radios](radio.md), or the [select](select.md).
- An answer that should be sent the moment it is pressed. Use [buttons](button.md): arrow keys move the choice in a set of radios and would send every choice they pass.

## Behavior

Picking a card checks its control and draws the brand edge at the emphasis width, inside the card so it keeps its size, with the mark filled. Under the pointer the card lifts onto the raised surface; pressed, it sinks onto the sunken surface. With `error`, every card takes the danger edge, picked or not. Disabled, the card sits on the disabled surface with its words dimmed; a picked disabled card keeps its edge in the disabled border color.

The radio mark is the default; with the checkbox mark, several cards can be picked. Inside a field group a card takes the group's name, error and required state, and a radio card follows the chosen value when the group holds it.

## Accessibility

### What it does for you

- The control is a real radio or checkbox, named by the title and described by the line, so a screen reader says the title, radio or checkbox, picked or not, then the line.
- Radios in one group take one stop for the Tab key and the arrow keys move the choice; Space picks.
- The focus ring goes around the whole card.

### What you must do

- Put a set of cards in a field group, whose legend names it.

### How to test

- Tab to a set of radio cards: one stop takes focus, with the ring around the whole card in both themes and in a forced-color mode. Move with the arrow keys, then press Space to pick.
- With a screen reader, a card is read as its title, a radio or a checkbox, picked or not, then its line, and the set by its legend.
- With text at 200% and the page 320 pixels wide, the title and line wrap and the card stays whole.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus to the card. Radios in one group take one stop. |
| Up Arrow, Down Arrow, Left Arrow or Right Arrow | Moves the choice to the next or previous radio card in the group. |
| Space | Picks a radio card, or toggles a checkbox card. |

## Usage rules

### Do

- Keep the title to a few words and the line to one sentence.
- Pre-select the most common choice when there is one.

### Don't

- Put buttons or links inside a card: a press anywhere picks it.
- Show more than about five cards in one group.
