# Fieldset

> A group of controls with a shared label. CSS component with two classes and no JavaScript.

`<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](/components/label.md).

## How to use it

**Plain HTML**

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/tokens/tokens.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/fieldset/fieldset.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/radio/radio.css">
<!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/lotek.css -->
```

```html
<fieldset class="lt-fieldset">
<legend class="lt-legend" data-required="symbol">Who are you applying on behalf of?</legend>


<div class="lt-radio-row">
  <input class="lt-radio" type="radio" id="self" name="applicant" value="self" checked />
  <label class="lt-label" for="self">Myself</label>
</div>


<div class="lt-radio-row">
  <input class="lt-radio" type="radio" id="other" name="applicant" value="other" />
  <label class="lt-label" for="other">Another person</label>
</div>
</fieldset>
```

**TypeScript**

```ts
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:

```tsx
import { lt } from "@lo-tek/interfaces/react"


<fieldset {...lt.fieldset({ state: "invalid" })}>
  <legend {...lt.legend()}>How would you like a reply?</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:

```ts
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](/type-safe-use.md).

| 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

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.

**Plain HTML**

```html
<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">We dispatch within three working days.</p>


<div class="lt-radio-row">
  <input class="lt-radio" type="radio" id="delivery-post" name="delivery" value="post" data-state="invalid" aria-invalid="true" />
  <label class="lt-label" for="delivery-post">Standard post</label>
</div>


<div class="lt-radio-row">
  <input class="lt-radio" type="radio" id="delivery-collect" name="delivery" value="collect" data-state="invalid" aria-invalid="true" />
  <label class="lt-label" for="delivery-collect">Collect from the office</label>
</div>


<p class="lt-error-text" id="delivery-error">Choose how you want the item delivered.</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

`disabled` on the `<fieldset>` disables everything inside, including fields added later:

```html
<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

| 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](/components/label.md), so one setting applies to both. See [Translation](/translation.md).

See [Customising](/customising.md) for how the variables and the layer work.

## 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 “I accept the terms” 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 you want the item delivered”, not “This field is required”.
