<!-- GENERATED by scripts/build-llms.mjs from llms/forms.md — do not edit this file. -->

# `lr-checkbox-group`

- **Import** `import '@aceshooting/lyra-ui/components/lr-checkbox-group.js';` (stable tag alias; registers the tag)
- **Compound usage registrations** `import '@aceshooting/lyra-ui/components/lr-checkbox.js';` — required by the consumer-supplied child tags in this reference; usage-only, not registration dependencies of `lr-checkbox-group`
- **Class** `LyraCheckboxGroup`, also available unregistered from `@aceshooting/lyra-ui/components/forms/checkbox-group/checkbox-group.class.js`
- **Family** `components/forms/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 6 parts, 7 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-checkbox-group`

Long group labels, hints, errors, and horizontal option labels wrap within the host in both logical
directions. The `Narrow RTL long options (320px)` story is the adversarial baseline; checkbox
targets keep their own fixed hit-area floor while the surrounding text wraps.

A form-associated collection of `<lr-checkbox>` children. Its readonly `value` is a defensive
`string[]` snapshot; each
selected value is submitted under `name` and `required` requires at least one selection.

**Properties:** `label`, `hint`, `errorText`, `value`, `customError` (`custom-error`), `name`,
`required`, `disabled`, `orientation: 'vertical' | 'horizontal' = 'vertical'`,
`withLabel`/`withHint` (`with-label`/`with-hint` SSR presence hints),
`accessibleLabel` (`aria-label`), and `size?: LyraSize` (reflected) — the optional size of the group's
**own** chrome, on the shared ladder and accepting both `2xs`/`xs`/`s`/`m`/`l`/`xl` and
`small`/`medium`/`large`. It scales the group's label type size and the gaps around and between its
options. When set, it temporarily projects the tier to every owned `<lr-checkbox>`, including
children added later. When omitted, each child retains its own authored tier.

An explicit group `size` is owner state, not a destructive rewrite: late child-size writes are
remembered while the group remains authoritative, and removing the group size, moving an option
out, or disconnecting the group restores the latest authored child value. This matches Web
Awesome's unset-default behavior.

**Slots:** default checkboxes, `label`, `hint`, `error`.
**Events:** a user toggle emits exactly one group-owned `input`, then `change`, then `lr-change`;
all three carry `{ value: string[] }`. The owned child's corresponding events are consumed at the
group boundary, so an ancestor does not receive a second, differently shaped sequence.
Programmatic child `checked`/`value` synchronization is silent and completes synchronously, so a
same-task `new FormData(form)` or validity query observes the same state as the child.
`lr-invalid` (no detail) is the group's one bubbling/composed native-validity alias.
**Refusing a toggle.** `lr-checkbox-group-toggle-request` is cancelable and fires *before* the owned
option flips. `detail: { value: string[], previousValue: string[], option: LyraCheckbox }` carries
the group value that **would** result, the value as it stands, and the checkbox the user acted on.
`preventDefault()` keeps the current state, so the option never flips at all rather than flipping
and snapping back — which is what lets a host refuse "uncheck the last remaining option"
(`detail.value.length === 0`) with no flicker — and no `input`/`change`/`lr-change` follows.
Assigning the group's `value` from a listener resolves the request the same way. The owned
checkbox's own `lr-checkbox-toggle-request` is consumed at the group boundary and republished under
this name, so an ancestor never receives two veto points for one interaction. The detail is a
detached, frozen snapshot; `option` is kept by identity. `LyraCheckboxGroupToggleRequestDetail` is
its exported interface — see **Exported TypeScript contracts** below for the full signature.
**Methods:** `getForm()` returns the group's owning form, including an external owner selected by
`form`. `setCustomValidity(message)` sets or clears a consumer-supplied error ("that
combination of topics is not available"): a non-empty message raises `customError` and blocks
submission, `''` restores the group's own computed validity so a required group with nothing checked
goes back to `valueMissing`. It survives every child toggle, slot change and form reset.
Session restore uses a `FormData` state containing the repeated selected strings; it is independent
of the control's current `name`, preserves duplicate-value cardinality, waits for early-arriving
option children, and falls back to an empty selection for malformed state. Restoration is silent.
`focus()` targets the first enabled checkbox, `blur()` releases whichever owned checkbox contains
focus, and `click()` activates (toggles) the first enabled checkbox. Native validity UI anchors to
that checkbox's focusable semantic owner. A required group also leases a localized visually-hidden
aggregate requiredness description onto its fieldset without replacing hint/error IDs; it does not
incorrectly mark every child checkbox required.
The fieldset exposes explicit stateful `aria-invalid`: visible property/slotted error chrome makes
it `"true"` immediately; otherwise only interacted intrinsic/custom invalidity does so, and the
valid/pristine state is explicitly `"false"`.
**CSS parts:** `form-control`, `form-control-label`, `options` / `form-control-input`, `hint`,
`error`.
**Disabled chrome.** A disabled group — its own `disabled` or an ancestor `<fieldset disabled>` —
dims `form-control-label`, `hint` and `error` to `--lr-opacity-disabled`. The dimming is keyed off
the UA-computed `:disabled` state (so the fieldset cascade reaches it) and is deliberately applied
to those three parts rather than the host: each owned `<lr-checkbox>` already dims itself, and a
host-level opacity would compound with it.
**The required marker.** `required` with a non-empty group `label` paints the library's shared
marker on `[part="form-control-label"]` — here the `<legend>` of the group's fieldset. It is the
one `::after` rule described under "The required-field marker" above, not a copy of it, so
`--lr-form-control-required-content`, `--lr-form-control-required-color` and
`--lr-form-control-required-offset` retune or suppress it here exactly as they do on `lr-input`. It
marks the **group**, not the individual checkboxes: an owned `<lr-checkbox>` has no label box of
its own and paints nothing. With no group label text the legend is hidden and no glyph is painted.

**Themeable custom properties:** `--lr-checkbox-group-row-gap` (default
`calc(var(--lr-form-control-height) * 0.1)`), the vertical gap between the group's label, options
and messages, and `--lr-checkbox-group-option-gap` (default
`calc(var(--lr-form-control-height) * 0.2)`), the gap between adjacent options — both scaled by
`size` through the shared control ladder. WA's `--gap` alias is the value used by the rendered
option layout and defaults to `--lr-checkbox-group-option-gap`.
`--lr-checkbox-group-invalid-border` (default `var(--lr-color-danger)`) independently retints the
invalid option-collection border without changing other danger-colored surfaces.

**`value` reads as a frozen defensive snapshot of child state, and assigning it mirrors back onto
the children.** The children remain the single source of truth. An internal sync recomputes `value`
on every child toggle, programmatic child `checked`/`value`/`disabled` update, `slotchange`,
`name`/`required` change, blur, and `form.reset()`. Mutating an obtained array cannot mutate the
group — assign a new array instead.
Only a checkbox whose nearest `lr-checkbox-group` ancestor is this group contributes; a nested
group owns its own descendants and form entries. `connectedCallback()` runs that sync before the
first render.

Assigning checks every child whose `value` (defaulting to `'on'`) appears in the array and unchecks
every other one; duplicate entries check that many same-valued children, and values naming no child
are ignored. `null`/`undefined` clear the selection. It is controlled input, so it emits no
`lr-change` — only user interaction does. An assignment made before the children exist (the shape of
a `.value=${...}` binding on first render) is applied once they arrive.

- **To preselect**, either set `checked` on the children (`<lr-checkbox value="a" checked>`) or
  assign the group's `value`.
- **To read the selection**, use this property or the `lr-change` event detail.
- **Give every child a distinct `value`.** `<lr-checkbox>`'s `value` defaults to `'on'`, so a group
  of undifferentiated children submits several identical `FormData` entries and the submitted data
  cannot say which one was checked. The group warns once per duplicated value when it sees this.
- Assignments migrate to writes on the intended children's `checked` state; a host value such as
  `['on']` cannot identify which default-valued occurrence was intended.
