Fieldset
<fieldset> with <legend> is the one way to give a set of radio buttons or checkboxes a shared label that screen readers announce before each option. A heading above the group looks the same, but is not heard.
In practice the component is two things: a reset of the browser’s border from 1996, and the same required and optional markers as on an ordinary Label.
How to use it
Section titled “How to use it”<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.40.0/src/tokens/tokens.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.40.0/src/components/css/fieldset/fieldset.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.40.0/src/components/css/radio/radio.css"><!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.40.0/dist/lotek.css --><fieldset class="lt-fieldset"><legend class="lt-legend" data-required="symbol">Who takes the job?</legend>
<div class="lt-radio-row"> <input class="lt-radio" type="radio" id="any-crew" name="crew" value="any" checked /> <label class="lt-label" for="any-crew">Any crew</label></div>
<div class="lt-radio-row"> <input class="lt-radio" type="radio" id="named-crew" name="crew" value="named" /> <label class="lt-label" for="named-crew">A crew the fixer names</label></div></fieldset>import "@lo-tek/interfaces/tokens.css"import "@lo-tek/interfaces/fieldset.css"import "@lo-tek/interfaces/radio.css"lt is used in two ways, and which one depends on whether you create the element or find it.
If you create the markup, you spread the attributes in. That is the route in React, Astro and anything else that renders:
import { lt } from "@lo-tek/interfaces/react"
<fieldset {...lt.fieldset({ state: "invalid" })}> <legend {...lt.legend()}>How does the crew report back?</legend></fieldset>The import goes to /react here, since the block is JSX. From there the keys are called className and htmlFor. In Astro and anything else without React you import @lo-tek/interfaces and get class and for.
If the element is already there, you set them on it. That is the route when you enhance HTML a server or a template has sent:
import { lt } from "@lo-tek/interfaces"
const group = document.querySelector<HTMLFieldSetElement>("#delivery")const title = document.querySelector<HTMLLegendElement>("#delivery-title")
lt.setAttributes(group, lt.fieldset({ state: "invalid" }))lt.setAttributes(title, lt.legend({ required: "symbol" }))The group and the label are two functions, because they are set on two elements. See Type-safe use.
// Once for the whole app, in the page template: every stylesheet, from the WebJar.head { link(rel = "stylesheet", href = LotekWebJar.CSS) }On a Kotlin or Java server, the same with kotlinx.html and lo-tek-kotlinx-html. The builder functions are in Lt, with an enum for each list of values:
import kotlinx.html.*import no.lotek.*import no.lotek.html.lt
fun FlowContent.crew() { fieldSet { lt(Lt.fieldset()) legend { lt(Lt.legend(required = RequiredMarker.SYMBOL)); +"Who takes the job?" } div { lt(Lt.radio.row) input { lt(Lt.radio()); id = "any-crew"; name = "crew"; value = "any"; checked = true } label { lt(Lt.label()); htmlFor = "any-crew"; +"Any crew" } } div { lt(Lt.radio.row) input { lt(Lt.radio()); id = "named-crew"; name = "crew"; value = "named" } label { lt(Lt.label()); htmlFor = "named-crew"; +"A crew the fixer names" } } }}Attribute on .lt-legend | Effect |
|---|---|
| (omitted) | An ordinary label |
data-required="symbol" | * in the danger colour |
data-required="text" | (required) in the danger colour |
data-optional="" | (optional) in a muted colour |
Choose one of data-required and data-optional, and stick to it throughout the form.
Help text and error text
Section titled “Help text and error text”The group can have help text and an error text like the fields otherwise. The connection for screen readers goes through aria-describedby on the <fieldset>, not on each button.
<fieldset class="lt-fieldset" data-state="invalid" aria-describedby="delivery-help delivery-error"><legend class="lt-legend" data-required="symbol">Delivery method</legend><p class="lt-help-text" id="delivery-help">The board does not meet crews.</p>
<div class="lt-radio-row"> <input class="lt-radio" type="radio" id="delivery-drop" name="delivery" value="drop" data-state="invalid" aria-invalid="true" /> <label class="lt-label" for="delivery-drop">Dead drop</label></div>
<div class="lt-radio-row"> <input class="lt-radio" type="radio" id="delivery-fixer" name="delivery" value="fixer" data-state="invalid" aria-invalid="true" /> <label class="lt-label" for="delivery-fixer">Through the fixer</label></div>
<p class="lt-error-text" id="delivery-error">Choose how the delivery reaches floor 222.</p></fieldset>data-state="invalid" on the group colours the label. The buttons themselves must get the same state one by one, since they are the ones with the border the user sees. They also need aria-invalid for the screen reader, and lt.radio({ state: "invalid" }) gives both.
data-state="success" colours the label green in the same way, and is used when the group is filled in and accepted. The colour is the only difference between the two, so it cannot stand alone: say in the text what is wrong or what is in order.
Disabling the whole group
Section titled “Disabling the whole group”disabled on the <fieldset> disables everything inside, including fields added later:
<fieldset class="lt-fieldset" disabled> <legend class="lt-legend" aria-disabled="true">Delivery method</legend></fieldset>Note that disabled on the group does not set disabled on each control. The property button.disabled is still false, while button.matches(":disabled") is true. If you check in JavaScript, use the latter.
Classes you can style
Section titled “Classes you can style”| Class | What it is |
|---|---|
.lt-fieldset | The group. The state comes from data-state. |
.lt-legend | The label of the group |
| Variable | Default |
|---|---|
--lt-fieldset-gap | var(--lt-spacing-2) |
--lt-label-required-symbol | " *" |
--lt-label-required-text | " (required)" |
--lt-label-optional-text | " (optional)" |
The last three are the text of the marker behind data-required and data-optional on .lt-legend. They are the same as on Label, so one setting applies to both. See Translation.
See Customising for how the variables and the layer work.
Accessibility
Section titled “Accessibility”<legend>should be the first element inside the<fieldset>. HTML requires it, and the markup is otherwise invalid. Browsers usually connect the first<legend>to the group wherever it is, but that is nothing to rely on: outside the content model, browsers and assistive technology can behave differently.- Use the group where the options belong to the same question. A single checkbox for “Clean only” does not need it.
- Do not put the whole form in one group. The label is then announced before every single field.
- The group’s error message should say what the user has to do: “Choose how the delivery reaches floor 222”, not “This field is required”.