Navigation

v0.0.4
Forms

Choice

A radio or checkbox option large enough to explain itself.

On this page

A choice is an option large enough to explain itself: a plan, a payment method, a setting with a sentence under it. It is a <label> around a native radio or checkbox, so the whole face is the target and the input keeps the state and the keys. At rest it is a flat face on one contour; chosen, it rises one step and its contour turns to ink.

Choice

<fieldset class="fieldset max-w(420) mx-auto">  <legend class="fieldset-legend">Plan</legend>  <label class="choice">    <input type="radio" class="radio" name="plan" value="hobby">    <span class="choice-body">      <span class="choice-title">Hobby</span>      <span class="choice-description">For personal projects and experiments.</span>    </span>    <span class="choice-end">Free</span>  </label>  <label class="choice">    <input type="radio" class="radio" name="plan" value="pro" checked>    <span class="choice-body">      <span class="choice-title">Pro</span>      <span class="choice-description">For teams that ship every day.</span>    </span>    <span class="choice-end">$20</span>  </label>  <label class="choice">    <input type="radio" class="radio" name="plan" value="enterprise" disabled>    <span class="choice-body">      <span class="choice-title">Enterprise</span>      <span class="choice-description">Not available in your region yet.</span>    </span>  </label></fieldset>

#How it reads its state

The choice has no state of its own. It asks the input inside it with :has(): :has(:checked) raises the face, :has(:focus-visible) draws the focus ring around the whole option instead of around the small input, and :has(:disabled) drops it to a well. Nothing is toggled by script and nothing can fall out of step with the form.

#Several at once

With checkboxes, any number can be chosen. A switch at the end makes a setting row.

Several at once

<label class="choice">  <input type="checkbox" class="checkbox" checked>  <span class="choice-body">    <span class="choice-title">Deployment succeeded</span>    <span class="choice-description">One email for each production deployment.</span>  </span></label><label class="choice">  <input type="checkbox" class="checkbox">  <span class="choice-body">    <span class="choice-title">Weekly summary</span>    <span class="choice-description">Traffic, errors and build minutes, every Monday.</span>  </span></label>

#In a grid

Choices are blocks, so a grid lays them side by side. Keep the titles short when they share a row.

In a grid

<label class="choice">  <input type="radio" class="radio" name="region" checked>  <span class="choice-body">    <span class="choice-title">Tokyo</span>    <span class="choice-description">hnd1</span>  </span></label><label class="choice">  <input type="radio" class="radio" name="region">  <span class="choice-body">    <span class="choice-title">Frankfurt</span>    <span class="choice-description">fra1</span>  </span></label><label class="choice">  <input type="radio" class="radio" name="region">  <span class="choice-body">    <span class="choice-title">Virginia</span>    <span class="choice-description">iad1</span>  </span></label>

#Accessibility

  • Group choices in a fieldset with a legend, and give radios one name.
  • The title and the description are both part of the label, so both are read as the option's name. Keep the description to one sentence.
  • The chosen state is drawn with ink and depth and by the input's own mark, never by hue.

#Classes

ClassDescription
choiceThe option, on a label around a radio, a checkbox or a switch; the focus ring is drawn around the whole option
choice-bodyThe words: a title and an optional description, on one edge
choice-titleThe option's name, inside choice-body
choice-descriptionOne muted sentence under the title, inside choice-body
choice-endA price, a badge or a figure at the end of the first line

#Custom properties

PropertyDefaultDescription
--choice-padding.875remPadding inside the option
--choice-radiusvar(--radius-control-lg)Corner radius

#Source

@master/ui/components/choice.css
/* * Choice: an option large enough to explain itself: a plan, a method, a setting with a sentence under it. * It is a <label> around a native radio or checkbox, so the whole face is the target and the input keeps the * state and the keys. At rest it is a flat face on one contour; chosen, it rises and its contour turns to ink. */@layer components {  .choice {    --choice-padding: .875rem;    --choice-radius: var(--radius-control-lg);    position: relative;    display: flex;    align-items: flex-start;    gap: .75rem;    min-width: 0;    padding: var(--choice-padding);    border-radius: calc(var(--choice-radius) * var(--corner-scale));    corner-shape: var(--corner-shape);    background: var(--color-surface-raised);    color: var(--color-text-body);    box-shadow: var(--shadow-flat);    font-size: .875rem;    line-height: 1.45;    cursor: pointer;    touch-action: manipulation;    -webkit-tap-highlight-color: transparent;    transition: box-shadow var(--duration-quick) var(--easing-glide);    @media (hover: hover) {      &:hover:not(:has(:checked, :disabled)) {        box-shadow: 0 0 0 .5px var(--color-line-mark);      }    }    /* Pressed: the option sinks into a well before it rises as the chosen one. */    &:active:not(:has(:checked, :disabled)) {      background: var(--color-surface-inset);      box-shadow: var(--shadow-pressed);      transition-duration: var(--duration-press);    }    /* Chosen: the face rises one step and its contour turns to ink: still a hairline, now at full strength. */    &:has(:checked) {      box-shadow:        inset 0 1px 0 var(--color-highlight),        0 0 0 .5px var(--color-text-strong), 0 0 0 1px color-mix(in oklab, var(--color-text-strong) 22%, transparent),        0 1px 2px -1px var(--color-shade-1), 0 2px 4px -2px var(--color-shade-2), 0 4px 8px -4px var(--color-shade-3);    }    /* The ring belongs to the whole option, so the input inside does not draw a second one. */    &:has(:focus-visible) {      @apply --focus-ring;      outline-offset: 3px;    }    &:has(:disabled) {      background: var(--color-surface-inset);      color: var(--color-text-disabled);      cursor: not-allowed;    }    @media (forced-colors: active) {      outline: 1px solid CanvasText;      outline-offset: -1px;      &:has(:checked) {        outline: 2px solid Highlight;        outline-offset: -2px;      }    }  }  .choice .checkbox:focus-visible, .choice .radio:focus-visible, .choice .switch:focus-visible {    outline: none;  }  /* The words: a title and an optional sentence, on one edge. */  .choice-body {    display: grid;    flex: 1 1 0;    gap: .125rem;    min-width: 0;  }  .choice-title {    color: var(--color-text-strong);    font-weight: var(--font-weight-control);    text-wrap: balance;    overflow-wrap: anywhere;  }  .choice:has(:disabled) .choice-title {    color: inherit;  }  .choice-description {    color: var(--color-text-muted);    font-size: .8125rem;    text-wrap: pretty;    overflow-wrap: anywhere;  }  /* A price, a badge or a figure at the end of the first line. */  .choice-end {    flex: none;    color: var(--color-text-strong);    font-variant-numeric: tabular-nums;  }}