Choice card
Choice card — a whole card acting as the control for one radio or checkbox.
Examples
<fieldset class="form-section form-section-tight">
<legend class="form-section-title">Plan</legend>
<input class="choice-input" type="radio" id="demo-plan-free" name="demo-tier" />
<label class="choice-card" for="demo-plan-free">
<span class="choice-card-title">Free</span>
<span class="choice-card-text">One project, community support.</span>
</label>
<input class="choice-input" type="radio" id="demo-plan-pro" name="demo-tier" checked />
<label class="choice-card" for="demo-plan-pro">
<span class="choice-card-title">Pro</span>
<span class="choice-card-text">Unlimited projects, email support.</span>
</label>
</fieldset>The input is the control and must be the label’s immediately preceding sibling — every checked and focused style reads it through +.
The group is .form-section on a real <fieldset> with a real <legend>: that pairing is the only thing in HTML that says "these choices are one question", and it is also what removes the UA fieldset border and puts a gap between the cards. .choice-card-row is a variant of a single card, not a wrapper for several. The group carries .form-section-tight: --space-5 is the distance between two questions and too much between two answers to the same one, and at the wider gap a set of cards reads as a list of unrelated panels.
<fieldset class="form-section form-section-tight">
<legend class="form-section-title">Add-ons</legend>
<input class="choice-input" type="checkbox" id="demo-multi-analytics" name="demo-addons" value="analytics" checked />
<label class="choice-card" for="demo-multi-analytics">
<span class="choice-card-title">Analytics</span>
<span class="choice-card-text">Page views, referrers and retention.</span>
</label>
<input class="choice-input" type="checkbox" id="demo-multi-backups" name="demo-addons" value="backups" checked />
<label class="choice-card" for="demo-multi-backups">
<span class="choice-card-title">Backups</span>
<span class="choice-card-text">Nightly, kept for 30 days.</span>
</label>
<input class="choice-input" type="checkbox" id="demo-multi-sso" name="demo-addons" value="sso" />
<label class="choice-card" for="demo-multi-sso">
<span class="choice-card-title">Single sign-on</span>
<span class="choice-card-text">SAML and SCIM provisioning.</span>
</label>
</fieldset>Change the input type and the card changes with it: type="checkbox" draws a square mark and lets the reader pick as many as they like, type="radio" draws a circle and lets them pick one.
Nothing else in the markup differs, and no class selects between them — the indicator is drawn from the control's own type, so it cannot claim one thing while the form does another. The mark is a pseudo-element and never a second thing to click; the real input is still the control.
<div class="form-section form-section-tight" role="group" aria-label="Add-ons">
<input class="choice-input" type="checkbox" id="demo-addon-a" />
<label class="choice-card choice-card-quiet choice-card-sm" for="demo-addon-a">
<span class="choice-card-title">Analytics</span>
</label>
<input class="choice-input" type="checkbox" id="demo-addon-b" checked />
<label class="choice-card choice-card-lg" for="demo-addon-b">
<span class="choice-card-title">Backups</span>
<span class="choice-card-text">Nightly, kept for 30 days.</span>
</label>
</div>Tokens 14
Level 2, declared on .choice-card itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --choice-card-bg | var(--color-surface-raised) |
| --choice-card-border-color | var(--color-border) |
| --choice-card-border-width | var(--border-width) |
| --choice-card-color | var(--color-on-surface) |
| --choice-card-gap | var(--space-1) |
| --choice-card-padding | var(--space-4) |
| --choice-card-radius | var(--radius-box) |
| --choice-card-mark-bg | var(--color-surface) |
| --choice-card-mark-border-color | var(--color-border-strong) |
| --choice-card-mark-checked-bg | var(--color-primary) |
| --choice-card-mark-color | var(--color-on-primary) |
| --choice-card-mark-gap | var(--space-3) |
| --choice-card-mark-glyph | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") |
| --choice-card-mark-size | 1.125rem |
Variants and states
Variants
.choice-card-quiet.choice-card-lg.choice-card-sm
Inside it
.choice-input.choice-card-row.choice-card-text.choice-card-title
State it reads
Read from the platform, never mirrored into a class that could disagree with it.
:checked:disabled:focus:hover
Before you ship it
What you have to do 1 requirements
- Group related cards with
<fieldset class="form-section">and a<legend class="form-section-title">, orrole="group"and a name. That pairing is the only thing in HTML that says "these choices are one question", and.form-sectionis also what strips the UA fieldset border, unpins the legend from it and puts a gap between the cards — a bare<fieldset>gives you all three defects at once and looks broken. There is deliberately no group class in this file..choice-card-rowis a variant of one card, laying its own contents out horizontally; it is not a wrapper for several, and using it as one was how the documentation shipped a group with a visible UA border and no gaps. Each input needs afor/idpair — without it, clicking the card does nothing at all.
src/css/components/choice-card.css · npx mostlycss add choice-card