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

# `lr-checkbox`

- **Import** `import '@aceshooting/lyra-ui/components/lr-checkbox.js';` (stable tag alias; registers the tag)
- **Class** `LyraCheckbox`, also available unregistered from `@aceshooting/lyra-ui/components/forms/checkbox/checkbox.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** 15 parts, 10 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`

A boolean form control. `role="checkbox"` with an `aria-checked` that can also be `"mixed"`, and a
visual box/checkmark. Structurally the same idea as `<lr-switch>` (form-associated via
`ElementInternals`, click and Space toggle) but with checkbox semantics.

Removing `error-text` safely omits the message while preserving native `null` property readback.
Explicit empty text stays empty; later supplied text renders normally.

**Properties:**

- `checked: boolean = false` — the live, non-reflecting state
- `defaultChecked: boolean = false` (canonical attribute `checked`, reflected) — the current reset
  default; changing it updates `checked` only while the live state is pristine
- `indeterminate: boolean = false` (reflected) — visual-only mixed state; does not affect `checked`,
  and is cleared back to `false` by any user interaction (click or keyboard), matching native
  `<input type="checkbox">`
- `disabled: boolean = false` (reflected)
- `required: boolean = false` (reflected — enforced via `internals.setValidity()`)
- `name: string = ''`
- `value: string = 'on'` — only contributed to form submission while `checked` (a native checkbox
  submits nothing at all, not even an empty string, while unchecked)
- `customError: string | null` (attribute `custom-error`) — reflected consumer validation message
- `hint: string = ''` — WA supporting text below the control
- `helpText: string = ''` (attribute `help-text`) — Shoelace alias for the same supporting-text
  surface; `hint` wins when both properties are set
- `errorText: string = ''` (attribute `error-text`) — owned error text associated with the inner
  checkbox; custom markup can use the `error` slot
- `size: LyraSize = 'm'` (reflected) — control size on the shared ladder, accepting both
  `2xs`/`xs`/`s`/`m`/`l`/`xl` and `small`/`medium`/`large`. It scales the box and its checkmark off
  the same values `lr-input`/`lr-select`/`lr-button` read, so controls of one `size` line up in a
  row. The slotted label keeps the library's standard control-label type size at every tier —
  restyle it through `::part(label)` if you want it to track the control.

**Events:** user toggles emit, in order, bubbling/composed `input`, the compatibility `lr-input`
alias, bubbling/composed `change`, then the compatibility `lr-change` alias (both aliases carry
`detail: { checked: boolean }`). Programmatic `.checked` assignments are
silent. Internal `focus`/`blur` are re-dispatched as bubbling, composed host events. `lr-invalid` (no detail) fires when a
validity check finds the checkbox invalid.
**Refusing a toggle.** `lr-checkbox-toggle-request` is cancelable and fires *before* `checked`
moves, on every user path (click, Space, and the host `click()` activation it forwards).
`detail: { checked: boolean }` is the state the control **would** take; `checked` itself still holds
the old value while the event dispatches. `preventDefault()` keeps the current state, so the box
never flips at all rather than flipping and snapping back, and none of
`input`/`lr-input`/`change`/`lr-change` follow. A listener can instead answer by assigning `checked`
itself during the dispatch, which suppresses the built-in write the same way — including when it
assigns the value the control already held, which a before/after comparison cannot detect. It does
not fire for a programmatic `.checked` assignment, a form reset, a session-state restore, or while
the control is disabled. Inside an `<lr-checkbox-group>` this event is consumed at the group
boundary and republished as `lr-checkbox-group-toggle-request`, exactly as the group already
translates the child's `input`/`change`/`lr-change`. A refused toggle also leaves the control
pristine: it does not count as the interaction that reveals `:state(user-valid)`/`:state(user-invalid)`,
so refusing a required checkbox's first toggle cannot flash a validation error for a change that
never happened. Blurring the control, or a `reportValidity()` call, still marks it interacted, which
is the native `:user-invalid` timing.

**Methods:** `focus(options?)`, `blur()`, and `click()` forward to the internal checkbox control;
`getForm()` returns its owning form (including an external owner selected by `form`).
`setCustomValidity(message)` sets or clears a consumer-supplied error ("those terms have been
superseded"): a non-empty message raises `customError` and blocks submission, `''` restores the
control's own computed validity so a required-and-unchecked box goes back to `valueMissing`. It
survives every toggle and a form reset; `setCustomValidity('')` or `resetValidity()` clears it.

**Slots:** default — rich label content rendered beside the semantic checkbox owner. Clicking
plain label content toggles like a native associated label; activating a nested link or button does
not toggle. If left empty, set `aria-label` on the host so the control still has an accessible
name. `hint` is the WA supporting-text slot;
`help-text` is the Shoelace spelling for the same described-by surface.
`error` supplies custom error markup on the same owned error surface as `errorText`.

The label, hint, and error wrappers can shrink and wrap at arbitrary boundaries inside a 320px LTR
or RTL allocation. The checkbox square and its shared interactive target remain fixed-size.

The default slot deliberately remains the checkbox's one visible, clickable label; there is no
separate top-of-field label property or slot. `form-control` wraps that checkbox plus its error and
hint, matching `lr-switch` without duplicating the label idiom.

The `checkbox`/`base` semantic role owner retains `--lr-icon-button-size` as its minimum inline and
block size at every tier. The visible `box` remains tied to `size`, so a label-less `2xs` checkbox
centres a compact square inside a 40px clickable target instead of inflating the glyph itself.

The label wrapper tracks flattened forwarding-slot assignment and later mutations. Its presence is
visual: an element-only icon or intentionally visible `aria-hidden` decoration keeps the wrapper,
independently of whether that node contributes to the accessible name. A host `aria-label` wins by
presence and is forwarded verbatim, including `aria-label=""`.

The internal `role="checkbox"` exposes explicit stateful `aria-invalid`. Visible property/slotted
error chrome makes it `"true"` immediately; otherwise it becomes true only after interaction while
intrinsic/custom validity fails, and explicitly returns to `"false"` when neither condition holds.

Host `aria-describedby` targets in the host's own root are resolved onto the internal
`role="checkbox"` through `ariaDescribedByElements`, so an externally-owned description remains
valid across the shadow boundary. In supporting browsers the explicit element list intentionally
leaves the internal role's serialized attribute empty; browsers without the reflected-reference
API keep the string fallback. The relationship tracks host attribute changes and clears when
unset.

**CSS parts:** `form-control` (outer checkbox/error/hint frame), `row` (the row wrapping the
checkbox owner and the label as siblings — the node to size or align when laying out a column of
checkboxes, since `base`/`checkbox` below is only the control box and its inline size tracks the box
rather than the row), `base` (compatibility name for the
semantic owner; use `checkbox`), `checkbox` (the interactive `role="checkbox"` owner; it is the
same node as `base`, while the rich default label is its sibling),
`box` / `control` (the small square showing the checkmark/indeterminate dash; while active it also
carries Shoelace's `control--checked` or `control--indeterminate` state token), `checkmark` plus
`checked-icon` or `indeterminate-icon` on the visible glyph, `label` (wrapper around the default
slot), `error`, and `hint` / `form-control-help-text` on the supporting-text wrapper.

**Themeable custom properties:** `--lr-checkbox-box-size` and `--lr-checkbox-label-indent` (both
below), plus shared tokens — `--lr-space-s`, `--lr-icon-button-size`,
`--lr-color-border/-surface/-on-brand/-brand/-text/-danger`, `--lr-radius`,
`--lr-transition-fast`, `--lr-focus-ring-width/-color/-offset`, `--lr-opacity-disabled`.
State paint is independently themeable through `--lr-checkbox-hover-border`,
`--lr-checkbox-active-border`, `--lr-checkbox-active-ring`, `--lr-checkbox-invalid-border`,
`--lr-checkbox-checked-bg`, and `--lr-checkbox-checked-border`; every default preserves the
corresponding brand/brand-quiet/danger token.

**`--lr-checkbox-box-size`** — the edge length of `[part='box']`, defaulting to
`min(var(--lr-icon-button-size), calc(var(--lr-form-control-height) * 0.7))`. Derived from the
active `size` tier's shared control height, so the box lines up with an
`lr-input`/`lr-select`/`lr-button` of the same `size` instead of carrying a scale of its own; at the
default `m` tier it resolves to `1.75rem`, exactly what the control shipped with before it had a
`size` at all. The `--lr-icon-button-size` cap is kept, so a consumer compacting that theme token
compacts this control with it. Set it to pin the box independently of the tier.

**`--lr-checkbox-label-indent`** — the inline distance from the control's start edge to the start of
the label text: the box plus the gap beside it. It defaults to
`calc(var(--lr-checkbox-box-size) + var(--lr-space-s))`, and the rendered gap is
_derived_ from it, so the advertised value and the real label offset cannot drift. Setting it on
the element (or on `lr-checkbox` in your own stylesheet) moves the label. The visible box
aligns with the inline start of its transparent hit target, so a compact box plus gap remains
clear of the label in both directions even when the hit target is wider than the box.

It is published so you can align your own per-option hint text under the label without re-deriving
that formula by reading the shadow styles. **But custom properties inherit down, not sideways**, so
a _sibling_ node in your tree cannot read it off the checkbox. Align a sibling by computing the
same formula from the `--lr-theme-*` inputs you control — the tier below is the default `m`;
substitute the one you actually use:

```css
.checkbox-hint {
  padding-inline-start: calc(
    min(
        var(--lr-theme-icon-button-size, 2.5rem),
        calc(var(--lr-theme-form-control-height-m, 2.5rem) * 0.7)
      ) + var(--lr-theme-space-s, 0.5rem)
  );
}
```

`--lr-checkbox-checked-bg` (default `var(--lr-color-brand)`) and `--lr-checkbox-checked-border`
(default `var(--lr-color-brand)`) recolor `[part='box']`'s background/border while `checked` or
`indeterminate` — a component-scoped indirection (the same pattern `lr-source-picker`'s own
`--lr-source-picker-checked-bg`/`-border` pair uses) so a consumer can retint just this control's
checked/indeterminate fill without hijacking the shared `--lr-color-brand` token everything else
reads.

WA's `--checked-icon-color` and `--checked-icon-scale` aliases directly control the visible
checkmark/dash color and scale.

**Optional peer deps:** none.

```html
<lr-checkbox name="terms" required>Accept the terms and conditions</lr-checkbox>
<script type="module">
  document
    .querySelector("lr-checkbox")
    .addEventListener("lr-change", (e) => console.log(e.detail.checked));
</script>
```

Form-associated via a directly-attached `ElementInternals` (not the shared `FormAssociated` mixin,
whose `value` accessor assumes a plain string default flow) with its own hand-rolled
`updateValidity()` — same shape as `<lr-combobox>`'s and `<lr-switch>`'s direct-`ElementInternals`
handling.
Session-history/autofill restoration uses four explicit state tokens: `checked`, `unchecked`,
`checked/indeterminate`, and `unchecked/indeterminate`. This preserves both public booleans while
keeping an unchecked control distinguishable from a checked control whose submitted value is an
empty string. Restoration updates state, form data, and validity synchronously without firing
`lr-change`.

**Known gotchas:**

- `checked` follows native dirty-state rules. A later `el.checked = true` assignment changes only
  the live state and never rewrites the attribute. Changing `defaultChecked` or the `checked`
  attribute updates the reset target but cannot overwrite a dirty live state; `form.reset()` uses
  that current default and makes the control pristine again.
- `indeterminate` is visual-only and silently clears on any user click/keypress — a consumer relying
  on it staying `true` after a user interacts with the box will be surprised.
- The rendered `aria-label` is copied from the host's own `aria-label` attribute at render time; if
  neither that nor slotted label text is present, the control has no accessible name.

---
