Skip to content

Choice card

A choice card wraps a native radio or checkbox in a card-shaped <label> — pick-a-plan tiles, shipping options, settings. Selection, keyboard roving, and form participation are all native; CSS marks the selected card via :has(:checked). No JavaScript.

Wrap the cards in a .re-choice-group <fieldset> with a <legend> naming the question. Each card is a .re-choice <label> containing a visible .re-radio control and a .re-choice__body with a title and optional description.

Plan
Live example
<fieldset class="re-choice-group">
  <legend>Plan</legend>
  <label class="re-choice">
    <input type="radio" class="re-radio" name="plan" value="starter" checked />
    <span class="re-choice__body">
      <span class="re-choice__title">Starter</span>
      <span class="re-choice__description">2 projects, community support</span>
    </span>
  </label>
  <label class="re-choice">
    <input type="radio" class="re-radio" name="plan" value="pro" />
    <span class="re-choice__body">
      <span class="re-choice__title">Pro</span>
      <span class="re-choice__description">Unlimited projects, priority support</span>
    </span>
  </label>
  <label class="re-choice">
    <input type="radio" class="re-radio" name="plan" value="team" />
    <span class="re-choice__body">
      <span class="re-choice__title">Team</span>
      <span class="re-choice__description">Everything in Pro, plus SSO and audit logs</span>
    </span>
  </label>
</fieldset>

data-orientation="horizontal" on the group lays equal-width cards in a wrapping row.

Billing period
Live example
<fieldset class="re-choice-group" data-orientation="horizontal">
  <legend>Billing period</legend>
  <label class="re-choice">
    <input type="radio" class="re-radio" name="billing" value="monthly" />
    <span class="re-choice__body">
      <span class="re-choice__title">Monthly</span>
      <span class="re-choice__description">$12 / month</span>
    </span>
  </label>
  <label class="re-choice">
    <input type="radio" class="re-radio" name="billing" value="yearly" checked />
    <span class="re-choice__body">
      <span class="re-choice__title">Yearly</span>
      <span class="re-choice__description">$120 / year — two months free</span>
    </span>
  </label>
</fieldset>

Swap in type="checkbox" + .re-checkbox for independent options — same markup otherwise.

Add-ons
Live example
<fieldset class="re-choice-group">
  <legend>Add-ons</legend>
  <label class="re-choice">
    <input type="checkbox" class="re-checkbox" name="addons" value="backups" checked />
    <span class="re-choice__body">
      <span class="re-choice__title">Daily backups</span>
      <span class="re-choice__description">Retained for 30 days</span>
    </span>
  </label>
  <label class="re-choice">
    <input type="checkbox" class="re-checkbox" name="addons" value="monitoring" />
    <span class="re-choice__body">
      <span class="re-choice__title">Uptime monitoring</span>
      <span class="re-choice__description">Alerts by email and webhook</span>
    </span>
  </label>
</fieldset>

Disable the input; the card follows via :has(:disabled).

Region
Live example
<fieldset class="re-choice-group">
  <legend>Region</legend>
  <label class="re-choice">
    <input type="radio" class="re-radio" name="region" value="eu" checked />
    <span class="re-choice__body">
      <span class="re-choice__title">Europe (Frankfurt)</span>
    </span>
  </label>
  <label class="re-choice">
    <input type="radio" class="re-radio" name="region" value="ap" disabled />
    <span class="re-choice__body">
      <span class="re-choice__title">Asia Pacific (Tokyo)</span>
      <span class="re-choice__description">Coming soon</span>
    </span>
  </label>
</fieldset>
  • The control stays visible. The card doesn’t hide the radio/checkbox behind styling — the native control carries the checked state, so assistive tech, Windows High Contrast, and muscle memory all get the standard control. The accent border + tint on the selected card is an enhancement, never the only cue (WCAG 1.4.1).
  • Name comes from the label. The wrapping <label> gives the input its accessible name from the visible title and description; the <legend> names the group. Keep both.
  • Focus ring on the card. Keyboard focus rings the whole card rather than the small control, so the focused option is obvious at a glance. Under forced colors the selected card’s border is re-established with the system Highlight color.
  • Arrow keys work natively. Radio groups rove with arrow keys; checkboxes Tab individually — both are browser behavior, nothing to configure.

See the accessibility guide for the project-wide stance.