# Field group

Several radios, checkboxes or choice cards under one question: a fieldset with its legend, help, and one error message for the group.

Block: `smbk-field-group`. 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 |
|---|---|---|---|
| `layout` | `vertical`, `horizontal` | `vertical` | Vertical stacks the options. Horizontal sets them in a row that wraps, only for a few short labels without lines under them. |
| `error` | on / off | off | The answer is invalid, such as a required question left empty. Every option takes its error look and one message under the options says what to do. |
| `disabled` | on / off | off | Every option is disabled; the question and its help stay readable. |

Parts: `smbk-field-group__legend`, `smbk-field-group__description`, `smbk-field-group__options`, `smbk-field-group__option`, `smbk-field-group__message`

## Markup

```html
<fieldset
  class="smbk-field-group smbk-field-group--layout-vertical"
  aria-describedby="groupfield-group-1-description"
>
  <legend class="smbk-field-group__legend"><span class="smbk-label smbk-label--tone-strong smbk-label--type-default"><span class="smbk-label__text">Delivery speed</span><span class="smbk-label__required" aria-hidden="true">*</span></span></legend>
  <p
    id="groupfield-group-1-description"
    class="smbk-field-message smbk-field-message--type-help smbk-field-group__description"
  >
    <span class="smbk-field-message__text">You can change it until the order ships.</span>
  </p>
  <div class="smbk-field-group__options">
    <label class="smbk-radio smbk-radio--size-md smbk-field-group__option">
      <input
        type="radio"
        class="smbk-radio__control"
        required=""
        aria-labelledby="radiofield-group-2-label"
        aria-describedby="radiofield-group-2-description"
        name="fg-configure"
        value="standard"
      >
      <span class="smbk-radio__circle" aria-hidden="true">
        <span class="smbk-radio__dot"></span>
      </span>
      <span class="smbk-radio__text">
        <span id="radiofield-group-2-label" class="smbk-radio__label">Standard</span>
        <span
          id="radiofield-group-2-description"
          class="smbk-radio__description"
        >Three to five working days.</span>
      </span>
    </label>
    <label class="smbk-radio smbk-radio--size-md smbk-field-group__option">
      <input
        type="radio"
        class="smbk-radio__control"
        required=""
        aria-labelledby="radiofield-group-3-label"
        aria-describedby="radiofield-group-3-description"
        name="fg-configure"
        value="express"
      >
      <span class="smbk-radio__circle" aria-hidden="true">
        <span class="smbk-radio__dot"></span>
      </span>
      <span class="smbk-radio__text">
        <span id="radiofield-group-3-label" class="smbk-radio__label">Express</span>
        <span
          id="radiofield-group-3-description"
          class="smbk-radio__description"
        >Next working day.</span>
      </span>
    </label>
    <label class="smbk-radio smbk-radio--size-md smbk-field-group__option">
      <input
        type="radio"
        class="smbk-radio__control"
        required=""
        aria-labelledby="radiofield-group-4-label"
        aria-describedby="radiofield-group-4-description"
        name="fg-configure"
        value="collect"
      >
      <span class="smbk-radio__circle" aria-hidden="true">
        <span class="smbk-radio__dot"></span>
      </span>
      <span class="smbk-radio__text">
        <span id="radiofield-group-4-label" class="smbk-radio__label">Collect</span>
        <span
          id="radiofield-group-4-description"
          class="smbk-radio__description"
        >From a pick-up point.</span>
      </span>
    </label>
  </div>
</fieldset>
```

## React

```tsx
import { FieldGroup } from './design-system/components/field-group/field-group.js';
import { Radio } from './design-system/components/radio/radio.js';

<FieldGroup layout="vertical" legend="Delivery speed" required description="You can change it until the order ships." name="fg-configure">
  <Radio value="standard" description="Three to five working days.">Standard</Radio>
  <Radio value="express" description="Next working day.">Express</Radio>
  <Radio value="collect" description="From a pick-up point.">Collect</Radio>
</FieldGroup>
```

## Stylesheet

```css
/* smbk-field-group — Field group. Figma "Field group" 7101:469 (State: Default, Disabled, Error × Layout: Vertical, Horizontal).
 * Several radios, checkboxes or choice cards under one question: a fieldset
 * whose legend is the label component in its strong tone, an optional line of
 * help under it, the options, and one field message for the whole group in
 * error. Each radio or checkbox in it is Figma's Choice option (7101:128): the
 * body text a step larger than a lone control's, a wider gap to its mark, and
 * an optional line under the label. Disabled, the fieldset disables every
 * option; the legend and the help keep their colors, as Figma draws them. */

.smbk-field-group {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

/* Floated, the legend is a row like the others, not drawn in the fieldset's edge; it still names the group. */
.smbk-field-group__legend {
  float: inline-start;
  padding: 0;
}

/* The question and its help sit together; the options and the message follow at the medium stack step. */
.smbk-field-group > :not(:first-child) {
  margin-block-start: var(--smbk-space-stack-md);
}

.smbk-field-group > .smbk-field-group__legend + .smbk-field-group__description {
  margin-block-start: var(--smbk-space-text-label);
}

.smbk-field-group__options {
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-stack-md);
}

/* Horizontal: the options hug their labels and wrap; only for a few short labels without lines under them. */
.smbk-field-group--layout-horizontal .smbk-field-group__options {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: flex-start;
  column-gap: var(--smbk-space-inline-2xl);
  row-gap: var(--smbk-space-stack-md);
}

/* Figma's Choice option: a radio or a checkbox with its label in Body MD and the mark a medium inline step
   away. Two classes, so it wins over the control's own row whichever stylesheet loads first. */
.smbk-field-group__option:is(.smbk-radio, .smbk-checkbox) {
  gap: var(--smbk-space-inline-md);
  font-family: var(--smbk-typography-body-md-font-family);
  font-size: var(--smbk-typography-body-md-font-size);
  font-weight: var(--smbk-typography-body-md-font-weight);
  line-height: var(--smbk-typography-body-md-line-height);
  letter-spacing: var(--smbk-typography-body-md-letter-spacing);
}

/* The help under the question holds a line of words, or blocks: paragraphs a paragraph apart, a list's items a line
   of list spacing apart with the bullets inside the group's edge, and code in the small code style, wrapping rather
   than running past the group. The message under the options takes the same blocks. */
:is(.smbk-field-group__description, .smbk-field-group__message) .smbk-field-message__text > :where(p, ul, ol, pre, blockquote) {
  margin: 0;
}

:is(.smbk-field-group__description, .smbk-field-group__message) .smbk-field-message__text > * + * {
  margin-block-start: var(--smbk-space-text-paragraph);
}

:is(.smbk-field-group__description, .smbk-field-group__message) .smbk-field-message__text :where(ul, ol) {
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-text-list);
  padding-inline-start: var(--smbk-space-inset-xl);
}

:is(.smbk-field-group__description, .smbk-field-group__message) .smbk-field-message__text :where(code, pre) {
  font-family: var(--smbk-typography-code-sm-font-family);
  font-size: var(--smbk-typography-code-sm-font-size);
  font-weight: var(--smbk-typography-code-sm-font-weight);
  line-height: var(--smbk-typography-code-sm-line-height);
  letter-spacing: var(--smbk-typography-code-sm-letter-spacing);
}

:is(.smbk-field-group__description, .smbk-field-group__message) .smbk-field-message__text pre {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
```

## Tokens it reads

```json
{
  "space": {
    "stack": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things stacked one above another, medium."
      }
    },
    "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."
      },
      "paragraph": {
        "$type": "dimension",
        "$value": "{{space.stack.lg}}",
        "$description": "Between two paragraphs (16). Text is not trimmed, so each line’s own leading adds to it."
      },
      "list": {
        "$type": "dimension",
        "$value": "{{space.stack.sm}}",
        "$description": "Between the items of a list (8)."
      }
    },
    "inline": {
      "2xl": {
        "$type": "dimension",
        "$value": "{{space.24}}",
        "$description": "Gap between things side by side, 2× large."
      },
      "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."
      }
    }
  },
  "typography": {
    "body-md": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "16px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    },
    "code-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist Mono, Geist Mono Fallback, ui-monospace, monospace",
        "fontSize": "12px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      },
      "$description": "Code SM: keys (Kbd) and short code in a line of text. Geist Mono."
    }
  }
}
```

## Purpose

Several radios, checkboxes or choice cards under one question. It is a fieldset whose legend asks the question, so a screen reader names the group before its first choice, with help under the question and one error message for the whole group. It is to a set of choices what the field is to one control.

## Anatomy

- The block, a fieldset with no edge of its own.
- The legend part, the question: the label component in its strong tone, with the required mark when an answer is needed.
- The description part, optional help under the question: a field message of the help type. A line of words, or blocks such as paragraphs, a list or code.
- The options part, the choices: two to five radios, checkboxes or choice cards, all of one kind.
- The option part, given to each radio or checkbox in the group: its label a step larger than a lone control's, its mark a wider step away, and an optional line under the label.
- The message part, in error, under the options: a field message of the error type.

## When to use

- One question answered by picking one of a few choices (radios) or any of them (checkboxes).
- A few choices that each need a line of explanation, as choice cards.

## When not to use

- A single control. Use the [field](field.md).
- More than about five choices. Use the [select](select.md).
- A choice that takes effect at once with two states. Use the [switch](switch.md) or the [segmented control](segmented-control.md).

## Behavior

The radios, checkboxes and choice cards inside take the group's name, its error and its required state, so each is written once, on the group. The radios share one name, the page's or one the group makes, so they act as one choice.

The group can hold which radio is chosen, as a select holds its option. Give it `value` and follow it with `onChange`, which is told the value of each radio chosen; or give it `defaultValue` and the group keeps the choice itself. Each radio, or radio choice card, is checked when its own `value` is the group's, and an empty value checks none. Without either, each radio keeps its own state, and `onChange` still hears every choice. Checkboxes keep their own state in either case.

The help under the question is a line of words, or blocks: paragraphs, a list, code, as a question written in Markdown has. Given words alone it is a paragraph; given anything else it is a div, which can hold blocks. Its paragraphs sit a paragraph apart, its list's bullets inside the group's edge, and its code in the small code style. Either way it describes the group, so a screen reader reads all of it with the choice.

Two layouts. Vertical stacks the options at the medium stack step. Horizontal sets them in a row that wraps, for two or three short labels with nothing under them.

In error, every option takes its error look and the message under the options says what to do. Given `error` with no message, the help text moves there as the message. Disabled, every option dims and is out of reach, while the question and its help stay readable; one option can also be disabled alone.

## Accessibility

### What it does for you

- The legend names the group: a screen reader says the question when focus moves into its first choice.
- The help and the message describe the group through `aria-describedby`, and the message is an alert, read as it appears.
- Radios take one stop for the Tab key, and the arrow keys move the choice; each checkbox is its own stop, and Space ticks it.
- Required puts the mark on the question and marks each radio required.
- In error, each option is marked invalid, and the message says what to change in words, with an icon.

### What you must do

- Write the legend as the question, so it is what a screen reader says on entering the group.
- For "at least one checkbox", check in the page and show the message: it has no native form.
- In error, write the message as what to change.
- Written by hand, help that holds blocks is a div with the field message's classes, its words in a div of the text part, since a paragraph cannot hold a list.

### How to test

- Tab into the group: for radios, focus lands on the chosen radio, or the first, and the arrow keys move the choice. Each checkbox is its own stop, and Space ticks it.
- With a screen reader, entering the group says the question, and the help and message are read with the choice.
- In error, the message is read as it appears and each option is read as invalid.
- With text at 200% and the page 320 pixels wide, the legend and options stay whole.
- In a forced-color mode, the options and the error icon are still visible.

### Keyboard

| Key | What it does |
|---|---|
| Tab or Shift + Tab | Moves focus into and out of the group: one stop for all its radios, one stop for each checkbox. |
| Up Arrow, Down Arrow, Left Arrow or Right Arrow | Among radios, moves focus to the neighboring radio and chooses it. |
| Space | On a checkbox, ticks or unticks it. |

## Usage rules

### Do

- Ask a question in the legend, and keep the option labels short.
- Pre-select the most common radio when there is one.

### Don't

- Mix radios, checkboxes and cards in one group.
- Lay long labels or options with lines under them out in a row.
