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

# `lr-radio`

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

A form-associated single-choice control. Use it alone or inside `lr-radio-group`.

An explicit live `checked` assignment marks the state dirty even if its boolean value is unchanged.
For example, assigning `checked = false` to an already-unchecked radio prevents a later
`defaultChecked = true` from selecting it until form reset. Pristine default propagation and
owning-group normalization retain their existing behavior.

Host `aria-describedby` references resolve onto the internal `role="radio"`, tracking unresolved
IDs, target replacement/removal/reinsertion, reconnect, and document adoption. The same behavior
applies to the button appearance of `lr-radio`.

**Properties:** live, non-reflecting `checked`; reflected `defaultChecked` (attribute `checked`);
reflected `customError: string | null` (attribute `custom-error`); `disabled`, `required`, `name`,
and `value`. A selected standalone radio submits its value through `ElementInternals`.
An empty `name` is canonicalized to an omitted attribute rather than reappearing as `name=""`.
`effectiveRequired` exposes the required state inherited from a containing radio group.
`effectiveName` and `effectiveSize` expose the owning group's aggregate projections while `name`
and `size` remain the option's authored state; late writes survive removal, reparenting, and group
disconnect/reconnect. `focus()`,
`blur()`, and `click()` forward to the internal radio control; `getForm()` returns the standalone
radio's owning form and the aggregate group's owning form while the radio is group-owned.

- `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 indicator 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 standard control-label type size at every tier — restyle it through
  `::part(label)` to make it track the control.
- `appearance: 'default' | 'button' = 'default'` (reflected) — WA's button presentation on the
  same `<lr-radio>` tag; it retains radio semantics, group ownership, value, and events
- `pill: boolean = false` (reflected) — rounds the control's own chrome into a pill instead of the
  shared control radius. A plain `<lr-radio>`'s indicator is a circle at every setting, so this is
  visible on `<lr-radio-button>`, which inherits this class and renders rectangular chrome; it is
  declared here so both tags carry one property with one meaning.

`setCustomValidity(message)` sets or clears a consumer-supplied error ("that plan is no longer
available"): a non-empty message raises `customError` and blocks submission, `''` restores the
control's own computed validity so a required-and-unselected radio goes back to `valueMissing`. It
survives every selection, every group-driven `required` change, and a form reset. A standalone
radio owns the error itself; an owned radio delegates it to the form-associated aggregate group.
`resetValidity()` clears the error through that same standalone-or-group owner.

`checked` follows native dirty-state semantics: changing `defaultChecked`/the `checked` attribute
updates the current reset default without overwriting a dirty live selection, and `form.reset()`
restores the current default.

**Events:** a standalone selection emits, in order, native-style composed `input`, `lr-input`,
native-style composed `change`, then `lr-change`; both aliases carry `{ checked, value }`. An owned
radio emits none of those child value events; its group emits the sole aggregate sequence described
below, so capture and bubble listeners cannot observe two differently shaped event sets. The
internal control's native `focus` and `blur` are re-dispatched as bubbling, composed host events.
`lr-invalid` (no detail) belongs to the standalone radio; an aggregate group emits its own alias.

**Slots:** default label content. In `appearance="button"`, `start`/`prefix` share the leading
wrapper and `end`/`suffix` share the trailing wrapper, matching `lr-radio-button`; changing away
from button appearance does not remove or rewrite the authored light-DOM content. Flattened
forwarding-slot assignment and later mutations keep the visual label wrapper synchronized;
element-only and visible `aria-hidden` decorations retain it. A host `aria-label` wins on the
internal radio by presence, including `aria-label=""`.

A standalone radio stays within its allocated inline size. Long or unbroken default labels wrap in
LTR and RTL while the indicator retains its fixed geometry; an exact-320px story covers both.

**CSS parts:** default appearance: `base`, `circle` / `control` (with Shoelace's
`control--checked` state token), `dot` / `checked-icon`, and `label`. Button appearance: `base`,
`button`, `control`, `button--checked` while selected, `start` / `prefix`, `label`, and `end` /
`suffix`. Empty leading, label, and trailing wrappers are hidden independently, so only present
content contributes `--lr-radio-button-gap` spacing.
Every `<lr-radio-button>` size tier keeps its interactive base at least 24px in both axes, including
an empty-label control; the visible density can still grow with the shared size ladder.

**Themeable custom properties:**

- `--lr-radio-circle-size` (default `min(var(--lr-icon-button-size), calc(var(--lr-form-control-height)
  - 0.7))`; `1.75rem`at the default`m`tier) — the edge length of`[part='circle']`, derived from
the active `size`tier's shared control height so a radio lines up with an`lr-input`/`lr-select`/`lr-button`of the same`size`.
- `--lr-radio-dot-size` (default `min(calc(var(--lr-radio-circle-size) * 0.5),
calc(var(--lr-form-control-height) * 0.3))`; `0.75rem` at `m`) — the edge length of `[part='dot']`,
  capped at half the circle so it can never outgrow its ring, whatever is done to either the ladder
  or the `--lr-icon-button-size` cap.
- `--lr-radio-radius` (default `--lr-radius-pill`) — the corner radius of the control's own chrome.
  A circular indicator is fully round at every setting; `<lr-radio-button>` changes the private
  default to the shared control radius and `pill` changes it back to a pill. An inherited or direct
  public value wins throughout.
- `--lr-radio-label-indent` (default `calc(var(--lr-radio-circle-size) + var(--lr-space-s))`) — the
  inline distance from the control's start edge to the start of the label text, i.e. the circle plus
  the gap beside it. The rendered gap is derived from it, so the advertised value and the real offset
  cannot drift; setting it on the element (or on `lr-radio` in your own stylesheet) moves the label.
  Exactly the same knob, purpose, and sideways-inheritance caveat as `--lr-checkbox-label-indent` —
  see `llms/components/lr-checkbox.md` for the formula to align a sibling hint element.

`--lr-radio-checked-border-color` (default `var(--lr-color-brand)`) and `--lr-radio-checked-dot-color`
(default `var(--lr-color-brand)`) recolor `[part='circle']`'s border and `[part='dot']`'s background
while `checked` — a component-scoped indirection (the same pattern `lr-checkbox`'s own
`--lr-checkbox-checked-bg`/`-border` pair uses) so a consumer can retint just this control's checked
ring/dot without hijacking the shared `--lr-color-brand` token everything else reads.
The pointer states are independently themeable with `--lr-radio-hover-border-color` (default
`var(--lr-color-brand)`), `--lr-radio-active-border-color` (defaulting through the hover border),
and `--lr-radio-active-ring-color` (default `var(--lr-color-brand-quiet)`).
WA's `--checked-icon-color` and `--checked-icon-scale` aliases feed the selected indicator's color
and scale.

```html
<lr-radio name="format" value="json">JSON</lr-radio>
```
