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

# `lr-toast`

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

---

## `lr-toast` / `lr-toast-item` / `toast()`

Stacking toast/notification region. Mirrors `<wa-toast>`/`<wa-toast-item>` under `lr-`.

### `lr-toast`

A placement-specific region. The imperative helper maintains one region for each
`ownerDocument` + `placement` pair; manually authored regions remain independent.

**Properties:**

- `placement: LyraToastPlacement = 'top-end'` (reflected) — one of `'top-start'|'top-center'|'top-end'|
'bottom-start'|'bottom-center'|'bottom-end'`. Every placement resolves inside one logical usable
  rectangle whose four edges are the greater of `--lr-space-l` and the matching safe-area token.
  Start/end follow direction, center placements use that rectangle's midpoint even when the inline
  safe-area insets are asymmetric, and an oversized stack is capped to its usable inline size.

**Methods:** `create(options: LyraToastOptions): Promise<LyraToastItem>` is the canonical form;
`create(message: string, options?: LyraToastCreateOptions)` remains as a compatibility overload.
`LyraToastOptions` contains `message`, optional `placement`, `ownerDocument`, `variant`, `duration`,
`size`, `withIcon`, `icon`, and `action`; `LyraToastCreateOptions` omits `message` and `placement`.
`icon` accepts text, a DOM node, a Lit template, or a target-document factory returning one of
those. Strings are always text, never HTML; cross-document nodes are imported into the region's
document. `action` creates a native button from `{ label, onClick }`. The options' `ownerDocument`,
and `placement`, when supplied directly to a region, must match that region. Calling `create()`
while the region is detached rejects immediately instead of leaving a render-dependent promise pending. `size` accepts
the canonical `2xs`/`xs`/`s`/`m`/`l`/`xl` values plus `small`/`medium`/`large`; either spelling is
preserved by the created item's getter and reflected attribute.

**Live members:** `stack: HTMLElement | null` returns the rendered `[part="stack"]` element, or
`null` before the render root is populated. Its identity remains stable across ordinary updates and
reconnection.

The region keeps exactly three items active. Later items enter a hidden, inert FIFO queue capped at
twenty; their show lifecycle and auto-dismiss timer do not start until promotion. Removing an active
item promotes the oldest queued item. A visible standalone item moved into a full region is
deactivated and timer-paused without replaying `lr-show`, then resumes from its remaining duration
when promoted; its progress ring resumes at the same elapsed fraction as that JavaScript countdown.
The visible stack is safe-area bounded and vertically scrollable, so all three active items and their
controls remain keyboard-reachable even with long localized content.

**Events:** `lr-toast-overflow` (noncancelable, `detail: { count: number }`) reports how many oldest
queued items were discarded when admission exceeded the twenty-item queue. Losses in one synchronous
burst coalesce into one event and one localized polite announcement; `create()` retains its existing
promise/result contract.

**Slots:** default (`<lr-toast-item>` children)

**CSS parts:** `stack`

**CSS custom states:** `visible` while at least one `lr-toast-item` is present in the region.

**Themeable custom properties:** `--lr-toast-gap` (default `var(--lr-space-s)`),
`--lr-toast-width` (default `var(--lr-size-28rem)`) — set directly on the `<lr-toast>` element.
The mapped `--gap` and `--width` names are compatibility aliases for those two Lyra-prefixed
properties; an explicitly set Lyra-prefixed property continues to take precedence.
Every `<lr-toast-item>` property below is also documented on the region, because custom properties
inherit into the items slotted inside it: one declaration on `<lr-toast>` retunes the whole stack.

**Optional peer deps:** none.

### `lr-toast-item`

A single notification.

**Properties:**

- `duration: number = 5000` (ms; `Infinity` or `<= 0` disables auto-dismiss)
- `size: '2xs'|'xs'|'s'|'m'|'l'|'xl'|'small'|'medium'|'large' = 'm'` (reflected — drives both `--lr-toast-padding` and the
  toast's own font-size through a private effective-size mapping, from a compact `2xs` up to a roomier `xl`;
  valid property and attribute writes round-trip without changing spelling)
- `variant: 'brand'|'success'|'warning'|'danger'|'neutral' = 'neutral'` (reflected)
- `withIcon: boolean = false` (attribute `with-icon`)

**Methods:** `async hide(): Promise<void>` — plays the hide animation, then removes itself from the
DOM. `toastItemElement: HTMLElement | null` exposes the live `[part="toast-item"]` surface (or
`null` before rendering) and remains the same node across ordinary updates/reconnection.

**Events:** `lr-show`, `lr-after-show`, `lr-hide`, `lr-after-hide`. `lr-show` and `lr-hide` are the
cancelable before-transition veto points. The item stays hidden and inert throughout `lr-show`;
vetoing its initial request releases it from the region without an after-event or timer. Re-entering
the same show/hide request from its own before-event coalesces onto that request, so the outer veto
remains authoritative and the lifecycle event fires once. Vetoing an auto-dismiss expiry leaves the
item visible and restarts the full current normalized `duration`; repeated vetoes retry at that same
interval. Vetoing a manual `hide()` leaves any active countdown at its current elapsed position. An
accepted show or hide interrupted by disconnection resumes after reconnection and emits its matching
`lr-after-*` event exactly once; no terminal event fires while the item is detached.

**Slots:** default (message), `icon`

**CSS parts:** `toast-item`, `accent`, `icon`, `content`, `close-button`, `close-icon`,
`close-icon__svg`, `progress-ring`, `progress-ring__base`, `progress-ring__indicator`,
`progress-ring__label`, `progress-ring__track`. The progress-ring tree is rendered for a finite,
positive auto-dismiss duration and surrounds the close glyph; its indicator pauses alongside the
auto-dismiss timer on hover or focus.

**Themeable custom properties:** `--lr-toast-accent-width` (default `var(--lr-size-4px)`),
`--lr-toast-show-duration`/`--lr-toast-hide-duration`
(`var(--lr-transition-base, 180ms ease-out)` — the show/hide lifecycle reads the resolved computed
transition duration and uses it for its completion fallback), `--lr-toast-padding`
(`var(--lr-space-m)`) and `--lr-toast-font-size` (`var(--lr-font-size-m)`) — their private defaults
follow `size`, from a compact `2xs` up to a roomier `xl` — and `--lr-toast-accent-color` (default
`var(--lr-color-border)`, with a private default that follows `variant` to that variant's loud
fill). Inherited or direct public values remain authoritative across every size and variant.

`--lr-toast-item-gap` (default `var(--lr-space-s)`) controls the gap between the item's icon,
message, and close action; `--lr-toast-item-radius` (default `var(--lr-radius)`) controls the item
surface and accent-bar start corners. They are deliberately separate from `--lr-toast-gap`, which
continues to control only the region's stack spacing.

The close button's four inherited state hooks are `--lr-toast-close-button-hover-bg` (default
`transparent`), `--lr-toast-close-button-hover-color` (default `var(--lr-color-text)`),
`--lr-toast-close-button-active-bg` (default `color-mix(in oklab, transparent,
var(--lr-color-mix-partner) var(--lr-color-mix-active))`), and
`--lr-toast-close-button-active-color` (default `var(--lr-color-text)`). Each is an inline fallback
at the relevant state, so setting one on the item or an ancestor rethemes only that close-button
state rather than the item surface, other close states, or the region stack.

The mapped names `--accent-width`, `--show-duration`, `--hide-duration`, and `--padding` alias their
respective Lyra-prefixed properties. Setting the Lyra-prefixed form explicitly wins over its alias.

**Optional peer deps:** none.

Once a non-vetoed toast starts showing, its normalized message is appended to Lyra's pre-mounted,
shared light-DOM announcement sink: assertive for `danger`/`warning`, polite otherwise. Changing a
visible toast between those urgency levels announces that message at the new urgency. The visible
item and stack themselves remain ordinary content, so an icon, action, and close button never become
part of an atomic live announcement. Auto-dismiss timer **pauses** on `pointerenter`/`focusin`, **resumes**
on `pointerleave`/`focusout`, with real elapsed-time bookkeeping (WCAG 2.2.1 timing-adjustable) —
hover and focus are tracked as independent pause reasons, so releasing only one (e.g. the pointer
leaves while focus remains, or vice versa) keeps the timer paused until _neither_ holds it anymore.
A `duration` change while the timer is actively counting down reschedules it immediately against
the new value instead of waiting for the next pause/resume cycle. A vetoed timer expiry restarts
that full normalized value; if hover/focus or a disconnect begins during the veto event, the retry
stays paused and starts from the full value only after the item resumes/reconnects.
Accessible message extraction follows same-root `aria-labelledby` references and observes their
targets, lookup roots, and every open shadow root actually traversed for text, including targets
outside the toast subtree. Traversal is bounded; when a ceiling is reached, the announcement carries
an explicit ellipsis and the close action uses the localized truncated-context template instead of
silently treating the bounded prefix as whole. If no prefix fits before a ceiling, both surfaces use
the localized `toastContentIncomplete` fallback so the missing content remains explicit.

### `toast()`

From the `toaster` controller — the ergonomic entry point, no manual `<lr-toast>` mounting
needed:

```ts
import { toast } from "@aceshooting/lyra-ui/components/overlays/toast/toaster.js";

toast("Saved");
toast({
  message: "Deleted",
  variant: "danger",
  icon: (ownerDocument) => ownerDocument.createTextNode("!"),
  action: {
    label: "Undo",
    onClick: (item) => {
      /*...*/
    },
  },
});
```

`toast(input: LyraToastOptions | string): ToastHandle`, where
`ToastHandle = { item: Promise<LyraToastItem>; dismiss: () => void }`. The canonical options are
shared byte-for-byte with the region's object-form `create()`, including `ownerDocument`, safe icon
payloads/factories, actions, and the long `small`/`medium`/`large` size aliases. It lazily mounts
(and re-mounts if removed) **one singleton `<lr-toast>` region per distinct `ownerDocument` and
`placement`** on that document's body — a call targeting one placement/document never relocates
toasts already showing in another. A foreign document must have the toast elements registered in
its own custom-element registry; otherwise the returned `item` promise rejects explicitly.

```html
<script type="module">
  import { toast } from "@aceshooting/lyra-ui/components/overlays/toast/toaster.js";
  document
    .getElementById("save-btn")
    .addEventListener("click", () => toast("Saved!"));
</script>
```

**Known gotchas:**

- the stack and each visible item have no live-region role. Their normalized message is appended as
  one child of a shared non-atomic light-DOM sink at the variant's urgency; icon, action, and close
  controls remain outside that sink. Later meaningful message changes add only the changed normalized
  message, and appending an action does not re-announce it.
- the close button's accessible name is derived from the toast's own message text (the first 40
  grapheme clusters when it must be shortened, falling back to bare `"Close"` only when the toast
  has no text content and extraction completed) rather than a bare `"Close"` on every instance — useful when several toasts
  are stacked and a screen-reader or switch-access user needs to tell their close buttons apart
  without activating one first. On a legacy engine without `Intl.Segmenter`, it retains the whole
  label rather than splitting a grapheme. The localized `closeWithTruncatedContext` template owns
  truncation punctuation and word order. Rich non-interactive message markup contributes its text,
  named-slot/icon and actionable content do not, and live message text mutations or reassignment
  update the name through nested forwarding slots. Hidden, inert, CSS-hidden and `aria-hidden`
  message branches are excluded. Same-root external `aria-labelledby` targets and the open shadow
  roots traversed for their text remain synchronized across reconnect, replacement, and adoption.
  A bounded traversal prefix is explicitly marked as incomplete; if no prefix is available, the
  localized `toastContentIncomplete` fallback keeps both the announcement and close name truthful.
  Observation, animation frames, elapsed-time clocks and completion/auto-dismiss timers follow the
  item's owner window after iframe adoption and cancel through the same window that scheduled them.
- pause/resume-on-hover/focus (the component's main accessibility differentiator), including the
  independent-hover-vs-focus pause reasons above, now has regression test coverage.
- `hide()` is idempotent (a second call while already hiding is a no-op) and `[part="close-button"]`
  gets `aria-disabled="true"` once hiding starts, so a stray extra click/Enter during the hide
  animation can't re-enter it. A disconnect during that animation pauses completion; reconnecting
  the same item resumes it and emits/removes exactly once.
- When the focused close/action control's toast finishes hiding, focus moves to an adjacent toast's
  close control, or back to the connected element that held focus before the toast when no adjacent
  item remains.
- Prefer the `toast()` helper over manually creating `<lr-toast>`/`<lr-toast-item>` — it already
  handles the singleton-region and remount-if-removed logic. The helper and `lr-alert.toast()` never
  fall back to an unbounded unknown region in an owner document where the controller is unregistered;
  unavailable alert requests are removed and their existing `Promise<void>` settles.

---
