Formsstable.fieldset
Checkbox / radio group
A question with checkboxes (several answers) or radio buttons (one answer), grouped under a legend.
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
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
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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | required | — | 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-]*$ |
legend | string | required | — | The question, shown as <legend> ("Mitgliedschaft", "Einwilligungen").min 1 chars |
type | "checkbox" | "radio" | required | — | radio: exactly one answer; checkbox: any number. |
name | string | optional | — | Form field name of all inputs; defaults to id. For checkboxes the server receives one value per ticked box. |
hint | string | optional | — | Help text below the legend, linked to the group. |
error | string | optional | — | Error message for the group; marks all inputs invalid. Say what to do: "Bitte wähle eine Mitgliedschaft." |
required | boolean | optional | false | An answer is required (native required on radios / a single checkbox). |
optional | boolean | optional | false | Append "(optional)" to the legend (in forms where most questions are required). |
options | array of objects | required | — | Answers in display order. min 1 items |
options[].value | string | required | — | Submitted value. |
options[].label | string | required | — | Visible answer text. min 1 chars |
options[].hint | string | optional | — | Secondary line under the answer ("CHF 480.– pro Jahr"). |
options[].checked | boolean | optional | false | Selected. Only one option for radios. Never pre-tick consent. |
options[].disabled | boolean | optional | false | Not 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
| Attribute | Values | Default | Description |
|---|---|---|---|
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
| Part | Description |
|---|---|
legend | The question. |
.field__hint / .field__error | Group hint and error (same parts as the form field). |
.check | <label> row wrapping one input — the whole row is clickable. |
.check__text / .check__hint | Answer text and its secondary line. |
States
| State | Description |
|---|---|
input:checked | Accent fill with check mark / dot. |
input[aria-invalid="true"] | Danger border. |
input:disabled | Muted, not-allowed cursor. |
:focus-visible | Global 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
Related: Form field · Segmented control · Switch · Scale input · Error summary