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

# `lr-stat`

- **Import** `import '@aceshooting/lyra-ui/components/lr-stat.js';` (stable tag alias; registers the tag)
- **Class** `LyraStat`, also available unregistered from `@aceshooting/lyra-ui/components/data/stat/stat.class.js`
- **Family** `components/data/` — 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** 14 parts, 18 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-stat`

KPI/stat card — value + unit + label + optional icon/trend/caption.

Passive slotted content preserves normal pointer activation and Control/Meta/Shift intent. The
platform primary modifier and Shift activate a new browsing context through the real anchor;
listeners may cancel that activation. Nested interactive controls retain their own actions.
Temporary target and relation values are restored after activation unless the listener changed those
attributes itself.

Removing `caption` or `sub` omits the attribute fallback without hiding assigned slot content.
Removal retains `null` property readback; explicit empty strings remain empty and later supplied
values render normally.

**Renamed in 8.0.0 — breaking:** `appearance` is now `frame`. Library-wide, `appearance` means only
"how a control fills itself" and `frame` means "whether a container draws itself as a bounded card";
this property was always the second. There is no alias — `appearance` on `<lr-stat>` is an unknown
attribute now, so a stat left on `appearance="plain"` silently renders full card chrome again.

**Properties:**

- `label: string = ''`
- `accessibleLabel: string | null = null` (attribute `aria-label`) — when `href` is safe, this
  host-level override names the real whole-card anchor; removing it restores the natural
  label/value/unit name
- `value: string = ''`
- `unit: string = ''`
- `href?: string` — when it resolves to a safe URL, the root is a real whole-stat `<a>`; unsafe
  URL schemes keep the stat non-interactive. The anchor is stretched behind the visible content,
  so public slots remain semantic siblings rather than interactive descendants of the link
- `target?: string` — forwarded to the anchor while `href` is active; a nonempty target derives
  `rel="noopener noreferrer"` rather than exposing a separately settable `rel`
- `variant: LyraVariant = 'neutral'` (reflected) — the library's shared
  one semantic-tone vocabulary, tinting `[part="value"]`. **`brand` is new in 8.0.0**, so a stat
  whose headline is the primary metric no longer has to borrow `emphasis` (which is a card-chrome
  accent) to read as branded
- `deltaPercent: number | null = null` (attribute `delta-percent`) — a finite percentage delta;
  `null` hides the trend pill and any non-finite assignment normalizes to `null`
- `caption: string = ''`
- `goodDirection: 'up'|'down' = 'up'` (attribute `good-direction`) — which trend direction counts
  as "good"; inverts arrow/color polarity for cost/latency/error-rate-style metrics where a
  _decrease_ is the win.
- `rows: readonly StatRow[] = []` (attribute: false) — `StatRow { readonly label: string; readonly
value: string; readonly exactValue?: string }`; at most the first 10,000 rows are snapshotted and
  frozen at assignment. Reassign `rows` after changing it. The snapshot renders as a simple
  label/value breakdown list (`[part="rows"]`/`[part="row"]`/
  `[part="row-label"]`/`[part="row-value"]`) beneath the caption, hidden entirely when empty. A row's
  optional `exactValue` mirrors the headline `exactValue`/`exact-value` pattern: rendered as a `title`
  tooltip on that row's `[part="row-value"]` and gives it `tabindex="0"`, independently per row —
  unset rows are unaffected.
- `emphasis: boolean = false` (reflected) — visual emphasis (e.g. for a "headline" stat in a group):
  a brand-colored accent edge, orthogonal to the status `variant`; status semantics still win over
  it — `emphasis` only additionally tints `[part="value"]` brand-colored when `variant` is still
  `'neutral'`, never overriding an actual `success`/`warning`/`danger` value color
- `exactValue: string = ''` (attribute `exact-value`) — rendered as a `title` attribute on
  `[part="value"]` for a hover tooltip (e.g. `value="$1.2K" exact-value="$1,204.37"`); also gives
  `[part="value"]` `tabindex="0"` (only when set) so the tooltip is reachable by keyboard focus, not
  just hover
- `sub: string = ''` — a secondary line distinct from `caption`, e.g. a comparison-period label,
  rendered as `[part="sub"]` between the trend pill and the caption; hidden entirely when unset
- `prose: boolean = false` (reflected) — CSS-only variant that shrinks/lightens `[part="value"]` and
  hides `[part="unit"]`, for rendering a loading/status message in place of a numeric value
- `compact: boolean = false` (reflected) — tighter card padding; same convention as `lr-empty`'s and
  `lr-widget`'s `compact`
- `frame: 'card'|'plain' = 'card'` (reflected) — container treatment, on the library-wide `frame`
  vocabulary. `'card'` keeps the bordered, filled, padded box that stretches to fill its
  parent; `'plain'` removes the border, background, padding, corner radius **and** the
  `block-size: 100%` stretch, so the stat can sit inline in prose, a toolbar, or a table cell.
  `plain` wins over `compact` when both are set (there is no padding left to tighten), and it drops
  `emphasis`'s accent edge — that edge is card chrome — while `emphasis`'s brand value tint still
  applies. A `plain` stat with a safe `href` swaps the card's border-color/lift hover affordance
  (invisible with no border) for an underline on `[part="value"]`; the `:focus-visible` ring is
  unchanged
- `orientation: 'vertical'|'horizontal' = 'vertical'` (reflected) — layout axis. `'vertical'` stacks
  label, value, trend, sub and caption. `'horizontal'` lays label, value+unit, trend, sub and caption
  out on a single wrapping baseline row; `[part="spark"]` and `[part="rows"]` have no sensible place
  on a text baseline and stay stacked on their own full-width line beneath that row

**Events:** none.

**Slots:** `start` (canonical leading icon), default (legacy leading-icon alias, retained as the
fallback; `start` takes precedence when both are filled), `caption` (rich caption content —
overrides the `caption` attribute when slotted content is provided), `spark` (a sparkline, e.g. `<lr-sparkline
slot="spark">`, or other compact trend visual — `lr-stat` only reserves the slot and doesn't
render one itself), `sub` (rich sub-line content — overrides the `sub` attribute when slotted content
is provided). In a linked stat, an interactive slotted descendant keeps its own focus and action;
clicking non-interactive slotted content still follows the whole-card link.

**CSS parts:** `base` (a `<div>`, or an `<a>` for a safe `href`), `icon`, `label` (carries `hidden`,
and is collapsed, whenever `label` is empty — a label-less stat leaves no blank line above the
value), `value-row`, `value`, `unit`, `trend`, `sub`, `spark`,
`caption`, `rows`, `row`, `row-label`, `row-value` — `[part="value"]` gets `aria-labelledby` pairing
it with `[part="label"]`'s generated id whenever `label` is non-empty (so tabbing straight to the
`exactValue`-focusable value still announces e.g. "Revenue $1.2K USD", including the visible unit,
not just the bare value); each
`[part="row-value"]` is paired the same way with its own row's `[part="row-label"]`.

**Themeable custom properties:** `--lr-stat-bg` (default `var(--lr-color-surface)`) is
`[part="base"]`'s RESTING background — the tier a dashboard sits at all day, and the companion to
the linked card's existing pressed `--lr-stat-link-active-bg` (whose own default now mixes from
`--lr-stat-bg`, so one override retints both); `frame="plain"` still drops the fill entirely.
`--lr-stat-padding` (default `var(--lr-space-m)`) and
`--lr-stat-gap` (default `var(--lr-space-xs)`) control `[part="base"]`'s padding and gap in every
rendering path — the default card, `compact` (own defaults `var(--lr-space-s)` /
`var(--lr-size-0-125rem)`), `frame="plain"` (own default `0` for padding), and the internal
`.linked-content` wrapper used when `href` is set, so one override reaches a linked and an
unlinked stat identically. `--lr-stat-trend-good-color` (default `var(--lr-color-success)`)
and `--lr-stat-trend-good-bg` (default `color-mix(in srgb, var(--lr-color-success) 8%,
transparent)`) — text/background of `[part="trend"]` when its polarity (per `goodDirection`) is
"good"; `--lr-stat-trend-bad-color` (default `var(--lr-color-danger)`) and `--lr-stat-trend-bad-bg`
(default `color-mix(in srgb, var(--lr-color-danger) 8%, transparent)`) — the "bad"-polarity
counterparts. All four are independent of the headline value's `variant="success"`/`"danger"` tint,
so retinting the trend pill doesn't also recolor the value, and vice versa.
`--lr-stat-value-brand-color` (default `var(--lr-color-brand)`),
`--lr-stat-value-success-color` (default `var(--lr-color-success)`),
`--lr-stat-value-warning-color` (default `var(--lr-color-warning)`), and
`--lr-stat-value-danger-color` (default `var(--lr-color-danger)`) independently color the headline
value for each non-neutral `variant`. `--lr-stat-emphasis-border-color` and
`--lr-stat-emphasis-value-color` (both default `var(--lr-color-brand)`) independently color the
emphasis accent edge and a neutral emphasized headline without retinting `variant="brand"`.
Linked-card interaction paint is independently themeable through
`--lr-stat-link-hover-border-color` (default `var(--lr-color-brand)`),
`--lr-stat-link-hover-shadow` (default `var(--lr-shadow-s)`),
`--lr-stat-link-active-border-color`/`--lr-stat-link-active-shadow` (defaulting to their hover
counterparts), and `--lr-stat-link-active-bg` (defaulting to the existing active color mix). These
are point-of-use fallbacks, so values inherit from a theme ancestor and a value on `lr-stat` wins.

**Optional peer deps:** none.

```html
<lr-stat
  label="Active users"
  value="1,204"
  delta-percent="4.2"
  variant="success"
>
  <svg slot="start">...</svg>
</lr-stat>
<lr-stat label="Memories" value="128" href="/memories"></lr-stat>
```

**Known gotchas:**

- When `href` makes the whole stat a link, exact-value spans keep their hover tooltips but omit
  their own `tabindex` to avoid nesting focus targets inside the anchor.
- Slotted buttons, links, and other controls are outside the stretched whole-card anchor. Their
  actions never also navigate the stat; use a host `aria-label` when the link destination needs a
  more specific name than the visible label/value/unit.
- no `aria-live` region wraps `value`/`deltaPercent` — an in-place update after first render still isn't
  proactively announced to screen readers. The trend pill's direction/polarity is no longer
  conveyed by icon rotation/color alone, though: a visually-hidden span now spells it out in plain
  language (e.g. "increased 4.2%, good" / "decreased 2%, bad" / "unchanged"), so a screen reader
  landing on the pill (rather than being live-notified of a change) gets the full meaning, not just
  an `aria-hidden` arrow glyph.
- **9.0 migration:** rename `trend`/`trend=` to `deltaPercent`/`delta-percent`; replace the `NaN`
  absence sentinel with `null`. Import shared `LyraVariant` and `LyraFrame` directly; the redundant
  `StatVariant` and stale `StatAppearance` aliases were removed. Calling `click()` on a linked stat
  now activates its whole-card anchor exactly once.

---
