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

# `lr-accordion`

- **Import** `import '@aceshooting/lyra-ui/components/lr-accordion.js';` (stable tag alias; registers the tag)
- **Class** `LyraAccordion`, also available unregistered from `@aceshooting/lyra-ui/components/layout/details/accordion.class.js`
- **Family** `components/layout/` — 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** 1 part, 6 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Documented with** `lr-details`, `lr-accordion-item` (same section below)
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-details`, `lr-accordion`, and `lr-accordion-item`

`lr-details` is a native-semantics disclosure panel; it mirrors `wa-details` / `sl-details`.
`lr-accordion` and `lr-accordion-item` mirror `wa-accordion` / `wa-accordion-item`: the group owns
mode, presentation, lifecycle events, and roving focus, while each item renders a heading button
and animated panel. The two components intentionally keep distinct vocabularies: accordion items
use `expanded`, `label`, and `expand()`/`collapse()`/`toggle()`, while `open`, `summary`, and
`show()`/`hide()` belong only to Details.

`lr-toggle` reports the direction and source of an accepted Details transition. Accordion
coordinates only its direct `lr-accordion-item` children; Details retains independent state and
optional grouping through a shared non-empty `name`.

**Breaking in 9.0.0:** an accordion coordinates direct `lr-accordion-item` children only. Direct
`lr-details` panels used to be accepted as well; they are not any more. A `lr-details` slotted into
an accordion today is ordinary content owning its own disclosure lifecycle — the group applies
neither its presentation, nor its single-panel invariant, nor its roving keyboard model, nor its
`lr-expand`/`lr-collapse` lifecycle to it, and `expandAll()`/`collapseAll()` skip it. Migrate both
the tag and its member vocabulary: `<lr-details summary="..." open>` becomes
`<lr-accordion-item label="..." expanded>`, and `show()`/`hide()` become
`expand()`/`collapse()`. `lr-details` on its own, outside an accordion, is unchanged.

**Accordion properties:**

- `mode: 'single' | 'single-collapsible' | 'multiple' = 'multiple'` (reflected). `multiple` allows
  any number of expanded items. `single` permits at most one and activating the expanded item is a
  no-op. `single-collapsible` permits at most one but allows zero.
- `iconPlacement: 'start' | 'end' = 'end'` (attribute `icon-placement`, reflected),
  `headingLevel: string = '3'` (attribute `heading-level`, reflected; `1`–`6` select that heading,
  `none` omits it, and every other value renders the documented h3 fallback), and
  `appearance: 'filled' | 'outlined' | 'filled-outlined' | 'plain' = 'outlined'` (reflected). The
  group applies all three to each direct `lr-accordion-item` whenever children or properties
  change.

**Accordion-item properties:** `label: string = ''`, `expanded: boolean = false` (reflected),
`disabled: boolean = false` (reflected), plus the same `iconPlacement`, `headingLevel`, and
`appearance` properties listed above. **Removed in 9.0.0:** inherited Details members `open`,
`summary`, `name`, `size`, `show()`, and `hide()`; use the canonical accordion-item members or use
`lr-details` when the Details contract is required. Accordion expansion policy likewise has one
authority: migrate `multiple` to `mode="multiple"`, and `multiple="false"` to the intended
`mode="single"` or `mode="single-collapsible"` behavior.

**Details properties:** `open: boolean = false` (reflected), `disabled: boolean = false`
(reflected — blocks activation and sets `aria-disabled="true"`; its native summary uses
`tabindex="-1"` while disabled so sequential navigation matches a disabled accordion-item
trigger), `summary: string = ''`, `name: string = ''` (reflected — disclosures with the same
non-empty name in one document or shadow root are mutually exclusive),
`appearance: 'filled' | 'outlined' | 'filled-outlined' | 'plain' = 'outlined'` (reflected),
`iconPlacement: 'start' | 'end' = 'end'` (attribute `icon-placement`, reflected and logical), and
`size`.

A host `aria-label`, when present, names the actual Details summary or accordion-item trigger;
it wins over summary/label content even when explicitly empty. When the host attribute is absent,
Details and accordion-item property/localized fallbacks keep their native name-from-content
behavior. An active accordion-item `label` slot contributes its normalized
accessibility-visible text as the trigger's explicit name; its rendered subtree is inert and hidden
from assistive technology so the trigger remains the sole action.

`size: '2xs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'small' | 'medium' | 'large' = 'm'` (reflected, new
in 8.0.0) is the library's shared size ladder, so a disclosure scales with the controls around it
instead of being the one fixed-density element in a compact panel. Both spellings of every tier are
accepted — `s`/`small`, `m`/`medium`, `l`/`large` — so markup migrated from either upstream needs no
attribute rewrite. `m` is the default and reproduces the disclosure this component had before `size`
existed. The tier drives two custom properties (below) rather than any `::part()` rule, so a tier
the ladder doesn't cover is a two-line override rather than a fork.

**Methods:**

- Accordion: `expandAll()` expands every direct enabled item only in `multiple` mode;
  `collapseAll()` collapses every direct expanded item. Nested accordion items are never included.
- Accordion item: `expand(): Promise<void>`, `collapse(): Promise<void>`, and
  `toggle(): Promise<void>` initiate the corresponding transition and settle after its rendered
  motion. Disabled items are unchanged. `focus()` and the host's `click()` target the trigger.
- Details: `show(): Promise<void>` expands and `hide(): Promise<void>` collapses. Each promise
  settles after its matching `lr-after-show` or `lr-after-hide`; a vetoed, disabled, or
  already-satisfied request resolves without changing state. Assigning `open` runs the same
  Details lifecycle. `show()` is a no-op while disabled; `hide()` can still close a disabled
  Details panel.

**Events:**

- `lr-expand`, `lr-collapse`, `lr-toggle-request`, `lr-after-expand`, `lr-after-collapse` —
  accordion group lifecycle.
- `lr-show`, `lr-hide`, `lr-toggle`, `lr-after-show`, `lr-after-hide` — Details lifecycle only.

On the accordion, `lr-expand` and `lr-collapse` fire before a direct item changes, are cancelable,
and carry `detail: { item }`. **New in 10.0.0:** a cancelable `lr-toggle-request`
(`detail: { collapsed, item }`) fires alongside the matching directional event for every transition,
including sibling auto-collapses in `single`/`single-collapsible` mode and `collapseAll()`. It carries
the direction in the detail rather than the event name, matching `<lr-code-block>`/`<lr-chat-message>`'s
`lr-toggle-request` convention, plus an `item` reference the single-panel siblings do not need (an
accordion's toggling entity is one of several children, so the event target alone cannot identify it).
`preventDefault()` on **either** event vetoes the transition — the two are a symmetric veto pair, not
a primary and a notification. Note `<lr-thinking-panel>`'s own `lr-toggle-request` spells its detail
`{ expanded }` rather than `{ collapsed }`; the two conventions are not fully unified. **Changed in 9.0.0:** `item` is now always a `LyraAccordionItem` —
it could previously also be a `LyraDetails`. The exported `LyraAccordionPanel` union that spelled
that has been removed; use `LyraAccordionItem`. An accepted transition finishes with the
non-cancelable
`lr-after-expand` or `lr-after-collapse`, carrying the same item. In `single` mode, activating the
already-expanded item is a no-op and emits no collapse lifecycle. Nested accordions own their own
triggers; an outer group does not close siblings or emit its own lifecycle for an inner item. Item
methods and group methods use this lifecycle too. When opening an item in a single mode, the previously expanded
sibling's cancelable collapse is consulted before the new panel changes state; vetoing it keeps the
old item open and cancels the new expansion, so the group never silently violates its one-item
invariant.

> **A nested `lr-accordion`'s events are not scoped to it — filter by target.** Every accordion
> event goes through the shared `emit()` helper with `bubbles: true, composed: true`, so an inner
> `<lr-accordion>` slotted inside an outer item sends its own `lr-expand`, `lr-collapse`,
> `lr-toggle-request`, `lr-after-expand` and `lr-after-collapse` straight through the outer group.
> A listener bound directly on the outer `<lr-accordion>` therefore also receives the inner
> group's — and their `detail.item` is an item of the inner group, so an outer handler that looks
> that item up among its own children finds nothing, or acts on a panel it does not own.
> Coordination itself is already scoped: an outer group never applies its single-panel invariant,
> roving keyboard model, or lifecycle to an inner group's items. It is only the listener that
> needs the guard, the same one `<lr-details>` and `<lr-dialog>` document for their own events:
>
> ```html
> <lr-accordion id="outer">
>   <lr-accordion-item label="Outer">
>     <lr-accordion>
>       <lr-accordion-item label="Inner">Inner content.</lr-accordion-item>
>     </lr-accordion>
>   </lr-accordion-item>
> </lr-accordion>
> <script type="module">
>   const outer = document.querySelector('#outer');
>   outer.addEventListener('lr-expand', (event) => {
>     if (event.target !== event.currentTarget) return; // a nested group expanded, not this one
>     // ...
>   });
> </script>
> ```

The Details events `lr-show` and `lr-hide` have no detail payload and are cancelable; preventing
either leaves the panel in its previous state. Accepted changes emit `lr-toggle` with
`detail: { open, source }`, then the non-cancelable `lr-after-show` or `lr-after-hide` once
rendering and motion settle. `source` is `user` for a summary click or keyboard activation,
`programmatic` for `show()`, `hide()`, or assigning `open`, and `peer` when another Details with
the same non-empty `name` closes this one. The full orders are `lr-show` → `lr-toggle` →
`lr-after-show` and `lr-hide` → `lr-toggle` → `lr-after-hide`. Initially open markup emits
nothing, and an interrupted transition drops its stale after-event. The `animating` CSS custom
state is present only between an accepted state change and that settled boundary, and is cleared
when the element disconnects.

> **A nested `lr-details`' events are not scoped to it — filter by target.** Every Details event
> goes through the shared `emit()` helper with `bubbles: true, composed: true` hardcoded, with no
> exception for this component. A `<lr-details>` nested inside another `<lr-details>` (as ordinary
> slotted content, e.g. inside `header-actions` or the default panel) has its own
> `lr-show`/`lr-hide`/`lr-toggle`/`lr-after-show`/`lr-after-hide` bubble straight through the outer
> panel, so a listener bound directly on the outer `<lr-details>` also receives the inner one's
> events — an inner disclosure opening or closing looks identical to the outer one doing the same,
> the same failure mode `lr-dialog`'s `lr-close` carries and documents in its own section. Guard on
> the target:
>
> ```html
> <lr-details id="outer" summary="Outer">
>   Some outer content.
>   <lr-details id="inner" summary="Inner">Inner content.</lr-details>
> </lr-details>
> <script type="module">
>   const outer = document.querySelector('#outer');
>   outer.addEventListener('lr-toggle', (event) => {
>     if (event.target !== outer) return; // the inner details toggled, not this one
>     // ...
>   });
> </script>
> ```
>
> This is deliberate, not a bug to fix: non-bubbling Details events would be a breaking change, and
> `event.target`/`event.currentTarget` already give every listener exactly what it needs to tell the
> two apart.

**Keyboard:** each direct enabled accordion item contributes one heading button. Exactly one is in
the tab order; ArrowDown/ArrowUp move cyclically, horizontal arrows provide the same next/previous
movement and swap under RTL, and Home/End jump to the first/last enabled item. Disabled items are
skipped. Enter and Space use the native button activation contract. Focus and key handling stay
inside the nearest nested accordion.

**Slots:** accordion has a default slot for direct items. Accordion item has default panel content,
`label`, and `icon`; `label` slot → `label` property → localized `"Details"` is the precedence
order. The label slot accepts rich visible markup, but its flattened subtree is inert and hidden
from assistive technology: do not place independent links, buttons, inputs, form state, or focus
targets there. The accordion-item `icon` slot follows the same flattened-tree inert and aria-hidden
visual contract, while the trigger button remains the sole action. Details has `summary`,
`header-actions`, `expand-icon`, `collapse-icon`, plus default content. `header-actions` renders
extra controls (e.g. a trailing "add" button) as a sibling of the private native `<details>` in the
complete header row, so they stay rendered, visible, and hit-testable while the panel is collapsed
or disabled. Activating one never toggles the panel: a click whose
composed path crosses `[part~="header-actions"]` is exempted from disclosure activation. Its wrapper
is hidden and reclaims layout space whenever the slot is empty.

**CSS parts:** accordion exposes `base`. Accordion item exposes `base` and `accordion-item` on the
same outer wrapper, plus `heading`, `button`, `label`, `icon`, `panel`, and `content`. Details
exposes `base` and `details` on the same outer container, plus `header`, `summary`, `icon`,
`header-actions`, and `content`. The native `<details>` is private; `content` is inside its
findable `hidden="until-found"` closed-state gate.
The Details icon wrapper also carries Shoelace's `summary-icon` alias, so either part name styles
the same node. `header-actions` is the wrapper around the `header-actions` slot.

Open panel content now fills and scrolls inside a bounded host: place `<lr-details>` (or an
ancestor of it) in a container with a resolved block size and, once open, `[part="content"]`
fills the remaining space below the summary row and scrolls its own overflow internally instead
of the panel growing past the host. This has no effect on an ordinary unsized disclosure — the
chain resolves to `auto` and the panel stays exactly as content-sized as before. It has no effect
on the closed state either: the private closed-state findability gate (`hidden="until-found"`)
is untouched, and the open/close lifecycle (`show()`/`hide()`, `lr-show`/`lr-toggle`/
`lr-after-show`/`lr-hide`/`lr-after-hide`) is unaffected.

For rich independent actions, reserve a useful basis on `header-actions` so the complete action
group wraps onto another row before its checkbox label becomes too narrow. The header already
wraps; its summary belongs to a private native-details flex item, so setting `flex` on
`::part(summary)` does not control that item's allocation. This recipe uses public parts and
ordinary light-DOM layout:

```html
<style>
  lr-details.responsive-actions::part(header-actions) {
    flex: 1 1 12rem;
    min-inline-size: min(100%, 12rem);
    padding: var(--lr-space-xs);
    box-sizing: border-box;
  }
  .details-action-group {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--lr-space-s);
    min-inline-size: 0;
  }
  .details-action-group lr-checkbox { flex: 1 1 auto; min-inline-size: 0; }
</style>
<lr-details class="responsive-actions" summary="Settings">
  <span slot="header-actions" class="details-action-group">
    <lr-checkbox size="s">Show distribution details</lr-checkbox>
    <lr-badge>Live</lr-badge>
  </span>
  Distribution settings.
</lr-details>
```

Import the granular details, checkbox and badge registration entries. Adjust the `12rem` basis
for the actual labels. Logical sizing supports RTL and narrower allocations; the checkbox remains
independently focusable and operable when the summary is disabled.

**Themeable custom properties:** accordion item exposes `--lr-accordion-item-spacing` (default
`var(--lr-form-control-padding-inline)`), `--lr-accordion-item-show-duration` and
`--lr-accordion-item-hide-duration` (both default `var(--lr-duration-base)`), and
`--lr-accordion-item-easing` (default `var(--lr-easing-standard)`). The mapped unprefixed names
`--spacing`, `--show-duration`, `--hide-duration`, and `--easing` remain accepted aliases and win
when set. Panel and icon transitions stop under `prefers-reduced-motion: reduce`.

Accordion appearance paint is independently inheritable: `--lr-accordion-outlined-bg` (default
`var(--lr-color-surface)`) and `--lr-accordion-outlined-border-color` (default
`var(--lr-color-border)`); `--lr-accordion-filled-bg` (default
`var(--lr-color-surface-raised)`) and `--lr-accordion-filled-border-color` (default `transparent`);
and `--lr-accordion-filled-outlined-bg` (default `var(--lr-color-surface-raised)`) plus
`--lr-accordion-filled-outlined-border-color` (default `var(--lr-color-border)`). Direct item
surfaces have matching `--lr-accordion-item-outlined-bg`, `--lr-accordion-item-filled-bg`, and
`--lr-accordion-item-filled-outlined-bg` hooks with the same surface fallbacks. Item trigger paint
uses `--lr-accordion-item-button-hover-bg` (default `var(--lr-color-brand-quiet)`) and
`--lr-accordion-item-button-active-bg` (default the existing active `color-mix(...)`). These hooks
are read as inline fallbacks rather than declared on the host, so an ancestor theme can set them.

Details exposes `--lr-details-font-size` (default
`var(--lr-form-control-font-size)`) — the text size of both the summary and the panel.
`--lr-details-spacing` (default `var(--lr-form-control-padding-inline)`) — the block rhythm: the
summary's block padding and the panel's trailing padding, kept equal so a stack of disclosures reads
evenly. Each `size` tier changes both private defaults from the shared ladder; a public value on an
ancestor or the element remains authoritative. Note that the spacing knob
deliberately reads the ladder's _inline_-padding value: a stacked panel wants generous block rhythm,
whereas the ladder's own block padding exists to fit text inside a fixed control height and would
collapse the summary row. The summary's and the panel content's padding can also be tuned
independently of each other and of `--lr-details-spacing`: `--lr-details-summary-padding-block` and
`--lr-details-summary-padding-inline` control the summary alone, and
`--lr-details-content-padding-block-end` and `--lr-details-content-padding-inline` control the
panel content alone. All four default to the same `--lr-details-spacing` resolution described
above, mirroring how `--lr-details-gap`/`--lr-details-radius` (below) are already independent of
the spacing knob. `--spacing` aliases the Details rhythm and remains the highest-precedence
override, ahead of these four as well. `--show-duration` and
`--hide-duration` (both default `var(--lr-duration-base)`) tune its icon transitions. Motion stops
under `prefers-reduced-motion`, so the `lr-after-*` events still settle promptly in that branch.
`--lr-details-gap` (default `var(--lr-space-s)`) independently controls the summary content/icon
gap, and `--lr-details-radius` (default `var(--lr-radius)`) controls the surface corners. Both use
inline fallbacks, inherit from ancestors, and remain independent of the `size` density ladder.
Details surface paint uses `--lr-details-outlined-bg` / `--lr-details-outlined-border-color`,
`--lr-details-filled-bg` / `--lr-details-filled-border-color`, and
`--lr-details-filled-outlined-bg` / `--lr-details-filled-outlined-border-color`; their defaults are
respectively the existing surface/border, brand-quiet/transparent, and brand-quiet/border values.
Summary interaction paint uses `--lr-details-summary-hover-bg` (default
`var(--lr-color-brand-quiet)`) and `--lr-details-summary-active-bg` (default the existing active
`color-mix(...)`). All eight are inheritable inline-fallback hooks, so they isolate one disclosure
theme without requiring shared-token changes or shadow-part selectors.

```html
<lr-details summary="Advanced options">Panel content</lr-details>
<script type="module">
  const panel = document.querySelector("lr-details");
  let ready = false;
  // Cancelable: veto the open until some precondition is met.
  panel.addEventListener("lr-show", (e) => {
    if (!ready) e.preventDefault();
  });
  panel.addEventListener("lr-after-show", () =>
    panel.querySelector("input")?.focus()
  );
  ready = true;
  await panel.show();
</script>
```

```html
<lr-accordion
  mode="single-collapsible"
  icon-placement="start"
  heading-level="2"
>
  <lr-accordion-item label="Account" expanded
    >Profile settings</lr-accordion-item
  >
  <lr-accordion-item>
    <strong slot="label">Notifications</strong>
    Notification settings
  </lr-accordion-item>
</lr-accordion>
<script type="module">
  const accordion = document.querySelector("lr-accordion");
  accordion.addEventListener("lr-expand", (event) => {
    if (event.detail.item.disabled) event.preventDefault();
  });
</script>
```
