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

# `lr-typing-indicator`

- **Import** `import '@aceshooting/lyra-ui/components/lr-typing-indicator.js';` (stable tag alias; registers the tag)
- **Class** `LyraTypingIndicator`, also available unregistered from `@aceshooting/lyra-ui/components/conversation/typing-indicator/typing-indicator.class.js`
- **Family** `components/conversation/` — 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** 5 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-typing-indicator`

A purely presentational "assistant is responding" presence cue — no events, no interactivity. A
consumer mounts it while a response is being generated and removes (or hides) it once real content
arrives. Three visual variants share one component rather than three separate tags, since callers
pick between them along a single axis (how the surrounding surface wants the cue to read) and
nothing else about the component differs: `dots` (default, three dots with a staggered bounce — the
classic "typing…" affordance for a standalone status line), `pulse` (a single breathing dot, a
quieter cue for a tight space, e.g. next to an avatar), `cursor` (a blinking vertical bar, meant to
sit inline at the tail end of streamed text still being appended to).

Removing `label` restores the localized thinking name while preserving an explicit host
`aria-label`. Later label assignments remain reactive.

`label-placement="after"` additionally renders `label` (or its localized "Thinking…" fallback)
visibly next to the animated shape, in a new `part="label"` element, mirroring `<lr-spinner>`'s own
`labelPlacement` vocabulary. The default, `"none"`, is unchanged from this component's
screen-reader-only rendering before this property existed: a single `.sr-only` text node, no
visible twin. That visible `part="label"` node is itself `aria-hidden="true"`: the host's own
`aria-label` already carries the identical string as the accessible name, so the visible copy is
never exposed as a second, redundant accessibility-tree node for the same text.

**Properties:**

- `shape: TypingIndicatorShape = 'dots'` (`'dots' | 'pulse' | 'cursor'`, reflected)
- `label: string = ''` — caller-supplied accessible name. Empty or whitespace-only values use the
  localized “Thinking…” fallback; an explicit host `aria-label`, including `aria-label=""`, wins.
  The status is not re-announced on every animation frame, only on mount and on later label changes
- `labelPlacement: TypingIndicatorLabelPlacement = 'none'` (`'none' | 'after'`, attribute
  `label-placement`, reflected) — where the accessible label renders. `'none'` (default) keeps
  `label` screen-reader-only, exactly as this component rendered before this property
  existed. `'after'` also renders it visibly next to the animated shape.
- `size: TypingIndicatorSize = 'm'` (reflected) — visual size on the library-wide ladder;
  `TypingIndicatorSize` is an alias of the shared `LyraSize`, so it accepts `2xs`/`xs`/`s`/`m`/`l`/
  `xl` plus the `small`/`medium`/`large` spellings of `s`/`m`/`l`. A presence cue has three usefully
  distinguishable sizes rather than six, so the ladder renders as three tiers: `2xs`/`xs`/`s`/
  `small` are compact (for dense layouts, e.g. inline with a message bubble), `m`/`medium` is the
  default standalone status-line size, and `l`/`large`/`xl` are roomy. Every accepted value matches a
  rule — none is silently inert

**Events:** none — purely presentational.

**Slots:** none.

**CSS parts:** `base` (the decorative, `aria-hidden`, wrapper around the animated shape), `dot`
(each of the three dots in the `dots` variant), `pulse` (the single pulsing dot in the `pulse`
variant), `cursor` (the blinking bar in the `cursor` variant), `label` (the visible, `aria-hidden`,
label, rendered only while `label-placement="after"`)

**Themeable custom properties:** `--lr-typing-dot-size` (default `var(--lr-space-s)`, i.e. `0.5rem`;
`0.375rem` on the compact tier, `var(--lr-space-m)` on the roomy one), `--lr-typing-gap` (default
`var(--lr-space-xs)`, i.e. `0.25rem`; `0.1875rem` compact, `var(--lr-space-s)` roomy),
`--lr-inline-cursor-width` (shared inline-cursor hook; default `var(--lr-size-0-125rem)`, compact
`0.09375rem`, roomy `0.1875rem`), `--lr-inline-cursor-height` (shared inline-cursor hook; default
`var(--lr-size-1em)`, unaffected by `size`),
`--lr-typing-dot-stagger-1` (default `600ms`, second dot), `--lr-typing-dot-stagger-2` (default
`1200ms`, third dot), and `--lr-typing-duration` (default `var(--lr-transition-ambient)`, i.e.
`1.8s ease-in-out`) — the compound duration/timing-function token every variant uses as its
animation cycle. `--lr-typing-duration` is a dedicated alias: it defaults to the library-wide
`--lr-transition-ambient` token (shared by every other ambient-looping component), but overriding
it retimes only this component, leaving `--lr-transition-ambient` itself — and anything else keyed
off it — untouched.

**Optional peer deps:** none.

```html
<lr-typing-indicator label="Assistant is responding…"></lr-typing-indicator>
<lr-typing-indicator
  label-placement="after"
  label="Assistant is responding…"
></lr-typing-indicator>
<lr-typing-indicator shape="pulse" size="s"></lr-typing-indicator>
<lr-typing-indicator shape="cursor"></lr-typing-indicator>
<lr-typing-indicator
  style="--lr-typing-duration: 900ms ease-in-out; --lr-typing-dot-stagger-1: 300ms; --lr-typing-dot-stagger-2: 600ms"
></lr-typing-indicator>
```

Accessibility: since this indicator typically mounts and unmounts around a real generation lifecycle
(appears when a response starts, disappears once one arrives) rather than emitting a stream of
updates of its own, it does **not** route through `<lr-live-region>`/the internal `Announcer` —
that machinery exists to coalesce many rapidly-changing announcements into one, and there is only
ever a single announcement here: the mount itself. `role="status"` plus an accessible name derived
from `label` is set both as `aria-label` on the host _and_ as a visually-hidden text node
(`.sr-only`) in the shadow tree, so the name survives even if only one of the two is picked up by a
given assistive-tech/browser pairing. The animated shape itself is `aria-hidden="true"` — it's
decorative; `label` is the entire accessible content, nothing narrates individual animation frames.

**Known gotchas:**

- under `prefers-reduced-motion: reduce`, every variant collapses to its plain, fully-visible resting
  state (`opacity: 1`, no transform, `animation: none !important`) rather than freezing on whatever
  frame the animation happened to be on — notably relevant for `cursor`, which would otherwise risk
  freezing on its invisible ("off") blink half.
- `--lr-typing-duration` (like the `--lr-transition-ambient` token it aliases by default) is a
  compound `duration timing-function` value and cannot be divided with `calc()`. When retiming it,
  override both stagger properties alongside it to preserve the default one-third/two-thirds dot
  phasing, as shown above.
- the compact tier (`2xs`/`xs`/`s`/`small`) shrinks the dot size, gap, and cursor width, but **not**
  `--lr-inline-cursor-height` (still `1em` at any size) — the cursor bar's height is meant to track
  surrounding text size via `1em`, not the component's own `size` property.
- the six-step ladder collapses onto three rendered tiers here, so `2xs` and `s` look identical, as
  do `l` and `xl`. That is deliberate: six distinguishable dot diameters do not exist inside a `1em`
  line box, and accepting a value no selector matches would leave it quietly rendering at the
  default tier.

---
