Checkbox / radio group

Use radios for one answer out of up to ~5 options and checkboxes for any number of answers (UX guide §6.3: for ≤ 5 choices prefer these over a select). For 2–4 very short options that switch a view or text, use the segmented control instead.

The whole row is the hit area (min. 44 px): clicking the label text ticks the box. Each option can carry a hint line, e.g. a price or an age range.

Hint and error belong to the whole group: they are linked to the fieldset with aria-describedby, and an error marks every input aria-invalid. Consent checkboxes are a group of their own and are never pre-ticked (UX guide §6.6).

required sets the native required attribute on radios and on a single checkbox (e.g. "Ich akzeptiere die Statuten"). "At least one of several checkboxes" can't be enforced natively — validate it in page code and show error.

Examples

Radio group with hints

Mitgliedschaft
Data
render({
  "id": "membership",
  "legend": "Mitgliedschaft",
  "type": "radio",
  "required": true,
  "options": [
    {
      "value": "single",
      "label": "Einzelmitglied",
      "hint": "CHF 480.– pro Jahr",
      "checked": true
    },
    {
      "value": "family",
      "label": "Familie",
      "hint": "ab 2 Personen, CHF 780.–"
    },
    {
      "value": "passive",
      "label": "Passivmitglied",
      "hint": "Zurzeit keine neuen Passivmitglieder",
      "disabled": true
    }
  ]
})
Markup
<fieldset class="fieldset" id="membership">
<legend>Mitgliedschaft</legend>
<label class="check"><input type="radio" id="membership-1" name="membership" value="single" checked required><span class="check__text">Einzelmitglied<span class="check__hint">CHF 480.– pro Jahr</span></span></label><label class="check"><input type="radio" id="membership-2" name="membership" value="family" required><span class="check__text">Familie<span class="check__hint">ab 2 Personen, CHF 780.–</span></span></label><label class="check"><input type="radio" id="membership-3" name="membership" value="passive" disabled required><span class="check__text">Passivmitglied<span class="check__hint">Zurzeit keine neuen Passivmitglieder</span></span></label>
</fieldset>

Consent checkboxes

Data
render({
  "id": "consent",
  "legend": "Einwilligungen",
  "type": "checkbox",
  "hint": "Du kannst das jederzeit im Profil ändern.",
  "options": [
    {
      "value": "photos",
      "label": "Fotos dürfen veröffentlicht werden",
      "hint": "Website, Newsletter, Social Media"
    },
    {
      "value": "newsletter",
      "label": "Newsletter per E-Mail erhalten"
    }
  ]
})
Markup
<fieldset class="fieldset" id="consent" aria-describedby="consent-hint">
<legend>Einwilligungen</legend>
<p class="field__hint" id="consent-hint">Du kannst das jederzeit im Profil ändern.</p>
<label class="check"><input type="checkbox" id="consent-1" name="consent" value="photos"><span class="check__text">Fotos dürfen veröffentlicht werden<span class="check__hint">Website, Newsletter, Social Media</span></span></label><label class="check"><input type="checkbox" id="consent-2" name="consent" value="newsletter"><span class="check__text">Newsletter per E-Mail erhalten</span></label>
</fieldset>

With error after submit

Bevorzugte Trainingszeit

Bitte wähle eine Trainingszeit.

Data
render({
  "id": "group-pref",
  "legend": "Bevorzugte Trainingszeit",
  "type": "radio",
  "name": "slot",
  "required": true,
  "error": "Bitte wähle eine Trainingszeit.",
  "options": [
    {
      "value": "tue",
      "label": "Dienstag, 18:00–19:15"
    },
    {
      "value": "thu",
      "label": "Donnerstag, 18:00–19:15"
    }
  ]
})
Markup
<fieldset class="fieldset" id="group-pref" aria-describedby="group-pref-error">
<legend>Bevorzugte Trainingszeit</legend>
<p class="field__error" id="group-pref-error"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#alert-circle"/></svg>Bitte wähle eine Trainingszeit.</p>
<label class="check"><input type="radio" id="group-pref-1" name="slot" value="tue" required aria-invalid="true"><span class="check__text">Dienstag, 18:00–19:15</span></label><label class="check"><input type="radio" id="group-pref-2" name="slot" value="thu" required aria-invalid="true"><span class="check__text">Donnerstag, 18:00–19:15</span></label>
</fieldset>

Data contract

What render(data) accepts — validated against this JSON Schema in development and tests. Unknown fields are rejected. The same schema is in components.json.

FieldTypeRequiredDefaultDescription
idstringrequired—Id of the fieldset; prefix for {id}-hint, {id}-error and the option ids {id}-1, {id}-2, … Must be unique on the page.
pattern ^[A-Za-z][\w-]*$
legendstringrequired—The question, shown as <legend> ("Mitgliedschaft", "Einwilligungen").
min 1 chars
type"checkbox" | "radio"required—radio: exactly one answer; checkbox: any number.
namestringoptional—Form field name of all inputs; defaults to id. For checkboxes the server receives one value per ticked box.
hintstringoptional—Help text below the legend, linked to the group.
errorstringoptional—Error message for the group; marks all inputs invalid. Say what to do: "Bitte wähle eine Mitgliedschaft."
requiredbooleanoptionalfalseAn answer is required (native required on radios / a single checkbox).
optionalbooleanoptionalfalseAppend "(optional)" to the legend (in forms where most questions are required).
optionsarray of objectsrequired—Answers in display order.
min 1 items
options[].valuestringrequired—Submitted value.
options[].labelstringrequired—Visible answer text.
min 1 chars
options[].hintstringoptional—Secondary line under the answer ("CHF 480.– pro Jahr").
options[].checkedbooleanoptionalfalseSelected. Only one option for radios. Never pre-tick consent.
options[].disabledbooleanoptionalfalseNot selectable now (e.g. group full); explain why in its hint.
JSON Schema
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id",
    "legend",
    "type",
    "options"
  ],
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^[A-Za-z][\\w-]*$",
      "description": "Id of the fieldset; prefix for `{id}-hint`, `{id}-error` and the option ids `{id}-1`, `{id}-2`, … Must be unique on the page."
    },
    "legend": {
      "type": "string",
      "minLength": 1,
      "description": "The question, shown as `<legend>` (\"Mitgliedschaft\", \"Einwilligungen\")."
    },
    "type": {
      "type": "string",
      "enum": [
        "checkbox",
        "radio"
      ],
      "description": "`radio`: exactly one answer; `checkbox`: any number."
    },
    "name": {
      "type": "string",
      "description": "Form field name of all inputs; defaults to `id`. For checkboxes the server receives one value per ticked box."
    },
    "hint": {
      "type": "string",
      "description": "Help text below the legend, linked to the group."
    },
    "error": {
      "type": "string",
      "description": "Error message for the group; marks all inputs invalid. Say what to do: \"Bitte wähle eine Mitgliedschaft.\""
    },
    "required": {
      "type": "boolean",
      "default": false,
      "description": "An answer is required (native `required` on radios / a single checkbox)."
    },
    "optional": {
      "type": "boolean",
      "default": false,
      "description": "Append \"(optional)\" to the legend (in forms where most questions are required)."
    },
    "options": {
      "type": "array",
      "minItems": 1,
      "description": "Answers in display order.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "value",
          "label"
        ],
        "properties": {
          "value": {
            "type": "string",
            "description": "Submitted value."
          },
          "label": {
            "type": "string",
            "minLength": 1,
            "description": "Visible answer text."
          },
          "hint": {
            "type": "string",
            "description": "Secondary line under the answer (\"CHF 480.– pro Jahr\")."
          },
          "checked": {
            "type": "boolean",
            "default": false,
            "description": "Selected. Only one option for radios. Never pre-tick consent."
          },
          "disabled": {
            "type": "boolean",
            "default": false,
            "description": "Not selectable now (e.g. group full); explain why in its hint."
          }
        }
      }
    }
  }
}

Markup & states

Root: fieldset.fieldset — usable without render() by writing the markup directly.

Attributes

AttributeValuesDefaultDescription
aria-describedby (on fieldset)"{id}-hint {id}-error"—Links group hint and error.
aria-invalid (on inputs)true—Error state; red border on every box.

Parts

PartDescription
legendThe question.
.field__hint / .field__errorGroup hint and error (same parts as the form field).
.check<label> row wrapping one input — the whole row is clickable.
.check__text / .check__hintAnswer text and its secondary line.

States

StateDescription
input:checkedAccent fill with check mark / dot.
input[aria-invalid="true"]Danger border.
input:disabledMuted, not-allowed cursor.
:focus-visibleGlobal focus ring on the box.

Accessibility

  • Groups use fieldset + legend, so screen readers announce the question with every option.
  • Each input is wrapped in its <label>; the hint line is part of the label text.
  • Radios in a group share one name; arrow keys move between them natively.
  • The error is text linked via aria-describedby, not only the red border.

Guidance

DoOrder options logically (most common first, or natural order).
DoUse a separate group for each consent; never pre-tick it.
Don'tDon't use a single checkbox for a yes/no question that needs an explicit answer — use two radios.
Don'tDon't use checkboxes for settings that apply immediately — that's a switch.

Related: Form field · Segmented control · Switch · Scale input · Error summary