Actual CSS

Choice Cards

Card-style radio and checkbox labels where the whole card acts as a clickable target.

Class reference

ClassKindDescription
.choice-cardComponentCard-style <label> around a native control.
.chipModifierCompact pill form: filter chips, tags.
.primary / .secondaryIntentAccent for the selected state.
  • Use .choice-card on a <label> wrapping a native <input type="radio"> or <input type="checkbox">. The input is visually hidden; in browsers without :has() it stays visible so selection state never disappears.
  • The control shows before any choice: a ring (radio) or a rounded box (checkbox) on the leading edge, level with the first line. Checked, it fills with a check, and the card takes a primary border and a subtle tint.
  • Each child element of the label sits on its own row beside the indicator — a title and a .muted detail need no wrapper. Wrap loose text in an element.
  • Stack cards for a list of options (a person, an address, a delivery method), or pair them with grid utilities for options compared side by side.
  • The cards already frame each option, so group them in a .field-group.flush: the fieldset keeps its legend and semantics without drawing a second box around the cards.
  • Supports disabled, checked, focus-visible, and invalid states; the resting ring turns danger with the border.
Deliver to
<fieldset class="field-group flush stack">
  <legend class="field-label">Deliver to</legend>
  <label class="choice-card">
    <input type="radio" name="address" value="home" checked />
    <strong>Home</strong>
    <span class="muted">12 Rue des Tanneurs, 1000 Brussels</span>
  </label>
  <label class="choice-card">
    <input type="radio" name="address" value="work" />
    <strong>Work</strong>
    <span class="muted">Avenue Louise 54, 1050 Ixelles</span>
  </label>
  <label class="choice-card">
    <input type="radio" name="address" value="pickup" />
    <strong>Pickup point</strong>
    <span class="muted">Collect within 7 days</span>
  </label>
</fieldset>
<div class="grid-3">
  <label class="choice-card">
    <input type="radio" name="cpu" value="1" checked />
    <strong>8-core CPU</strong>
    <span class="muted">32 GB RAM</span>
  </label>
  <label class="choice-card">
    <input type="radio" name="cpu" value="2" />
    <strong>6-core CPU</strong>
    <span class="muted">24 GB RAM</span>
  </label>
  <label class="choice-card">
    <input type="radio" name="cpu" value="3" />
    <strong>4-core CPU</strong>
    <span class="muted">16 GB RAM</span>
  </label>
</div>
Features
<fieldset class="field-group">
  <legend class="field-label">Features</legend>
  <div class="grid-3">
    <label class="choice-card">
      <input type="checkbox" name="features" value="wifi" checked />
      <strong>WiFi</strong>
      <span class="muted">Dual-band 2.4/5 GHz</span>
    </label>
    <label class="choice-card">
      <input type="checkbox" name="features" value="bluetooth" />
      <strong>Bluetooth</strong>
      <span class="muted">5.3, low energy</span>
    </label>
    <label class="choice-card">
      <input type="checkbox" name="features" value="nfc" disabled />
      <strong>NFC</strong>
      <span class="muted">Coming soon</span>
    </label>
  </div>
</fieldset>

Validation follows the shared pattern: aria-invalid="true" on the input (set manually or by the .needs-validation enhancer) turns the card border to danger, and a .field-error inside a .field wrapper is shown.

Plan
Choose a plan to continue.
<fieldset class="field field-group">
  <legend class="field-label">Plan</legend>
  <div class="grid-3">
    <label class="choice-card">
      <input type="radio" name="plan" value="starter"
             aria-invalid="true" aria-describedby="plan-error" />
      <strong>Starter</strong>
      <span class="muted">1 project</span>
    </label>
    <label class="choice-card">
      <input type="radio" name="plan" value="pro"
             aria-invalid="true" aria-describedby="plan-error" />
      <strong>Pro</strong>
      <span class="muted">Unlimited projects</span>
    </label>
    <label class="choice-card">
      <input type="radio" name="plan" value="team"
             aria-invalid="true" aria-describedby="plan-error" />
      <strong>Team</strong>
      <span class="muted">Unlimited + SSO</span>
    </label>
  </div>
  <span class="field-error" id="plan-error" role="alert">
    Choose a plan to continue.
  </span>
</fieldset>

Sizes follow the shared .sm and .lg typography scale; the check indicator follows through its em sizing. Padding is density, so use a .compact or .spacious context to change it. Intent classes (.primary, .secondary) are supported on the .choice-card element.

Corner badge

A tile whose content is the choice itself — a product image, a centred plan name — may want no indicator column, and a check badge over its corner once selected (radio/checkbox tile, selected tick, check in the corner). That is a recipe on the card's own ::before (disc) and ::after (check) layers, written in application CSS after the framework:

.choice-tile {
  --choice-card-check-size: 1.5em;

  display: block;
}

.choice-tile::before,
.choice-tile::after {
  position: absolute;
  inset-inline-end: -0.375em;
  inset-block-start: -0.375em;
  margin: 0;
}

.choice-tile::before {
  border: 0;
  box-shadow: 0 0 0 2px var(--surface-opaque);
  transform: scale(0);
}

.choice-tile:has(:checked)::before {
  transform: scale(1);
}
<label class="choice-card choice-tile">
  <input type="radio" name="cpu" value="1" checked />
  <strong>8-core CPU</strong><br />
  <span class="muted">32 GB RAM</span>
</label>

The badge overhangs the corner, so no ancestor up to the grid may clip with overflow: hidden. The Neon Ramen item page uses it for its sizes and toppings.

Chips

Add .chip for the compact pill form: filter chips, selectable tags, toggle pills, multi-select facets. A radio group gives single-choice chips. A leading dot turns into the check in a fixed slot, so checking never moves the label. Use .cluster for a wrapping row. A chip is sized like a control: .btn and .input height, --control-pad-x inline padding, .sm / .lg scale. A chip is a <label> around its checkbox/radio — never nest a .close or any other button inside it. A removable value (combobox tag) belongs to the widget that produced it, or to a sibling .badge + .close, not to the chip label.

Fruit
<fieldset class="field-group">
  <legend class="field-label">Fruit</legend>
  <div class="cluster">
    <label class="choice-card chip">
      <input type="checkbox" name="fruit" value="pear" checked />
      Pear
    </label>
    <label class="choice-card chip">
      <input type="checkbox" name="fruit" value="apple" />
      Apple
    </label>
    <label class="choice-card chip">
      <input type="checkbox" name="fruit" value="plum" disabled />
      Plum
    </label>
  </div>
</fieldset>

The dot is ::before: a full-chip layer clipped to a circle() centred on the check slot (--choice-chip-dot). A theme can grow it into a selected fill by widening the clip, as the material theme does.

The check uses --state-selected-text when the theme sets it, including on chips with an explicit intent. Otherwise it follows the chip's intent.

CSS hooks

  • --choice-card-pad — inner padding.
  • --choice-card-gap — space between the indicator column and the label.
  • --choice-card-radius — corner radius.
  • --choice-card-check-size — size of the indicator, and the width of its column.
  • --choice-card-border / --choice-card-bg — resting border and background; the checked state and hover rebind them, so a checked override needs :has(:checked).
  • --choice-chip-dot — read-only: the chip's check-slot centre as a circle() position. It is declared on the chip's ::before, so read it in a .choice-card.chip…::before rule.

Plain .check / .radio controls expose two hooks:

  • --choice-control-size — the control's own size, in em off the surrounding type. The switch track derives from it too, so a checkbox and a switch on the same line stay proportionate. A .choice.sm or .choice.lg scales it through the surrounding typography.
  • --choice-control-nudge — block correction added to the derived first-line offset, for every control in the row. Left unset, each control centres itself on the first line's cap height, measured from its own block size — so a checkbox and a taller switch stay level with each other, and with the text, whatever the font, line-height or control size.
Actual CSS

Search documentation