Stat
Free · MITHeadless, accessible, and styled entirely through the attribute surface below — bring your own CSS system, or start from the reference theme.
Install
Without a flag you get no styling at all — plain HTML carrying the attribute surface below. --theme copies the same theme in two dialects;--bridge lumos re-points either one onto Lumos for Astro's tokens (it needs a theme to re-point, and Lumos itself, already in your project).
Unstyled — behavior only, you write the CSS
npx astrocraft-ui add display/statPlain-CSS theme — no build step
npx astrocraft-ui add display/stat --theme cssTailwind theme — needs Tailwind v4
npx astrocraft-ui add display/stat --theme tailwindPlain-CSS theme, re-pointed onto Lumos tokens
npx astrocraft-ui add display/stat --theme css --bridge lumosTailwind theme, re-pointed onto Lumos tokens
npx astrocraft-ui add display/stat --theme tailwind --bridge lumosLive demo
Stat
A KPI row is a description list, which is why "Revenue" and the number under it are associated rather than merely adjacent. Each delta announces its direction as a WORD — green-up / red-down is the canonical "colour as the only carrier of meaning" failure.
- Revenue
- £12,480Increased 12.4%
- Churn
- 2.1%Decreased 0.4pp
- Seats
- 318No change 0
Attribute surface
With no class-merging machinery, these attributes are the whole extension point. State is a data-attribute or a native attribute; visibility is the hiddenattribute. Generated from source by pnpm slots.
| Component | Slots | Variants | Runtime state | Native state |
|---|---|---|---|---|
| `Stat.astro` | `stat` | — | — | — |
| `StatDelta.astro` | `stat-delta` `stat-delta-direction` | `data-trend`: `up` · `down` · `flat` | — | — |
| `StatGroup.astro` | `stat-group` | `data-columns`: `1` · `2` · `3` · `4` | — | — |
| `StatLabel.astro` | `stat-label` | — | — | — |
| `StatValue.astro` | `stat-value` | — | — | — |
Source
What the command copies — 6 files, alias-free so the copy lands standing alone. This is the whole component; there is nothing else.
---
// src/components/ui/display/stat/Stat.astro — StatGroup compound part (see ../../README.md).
// One KPI: the <div> that groups a <dt> with its <dd>s. A plain <div> is the ONE non-<dt>/<dd> child
// HTML allows inside a <dl>, which is what makes a card-per-stat layout legal rather than a hack.
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"div">;
const { class: className, ...rest } = Astro.props;
---
<div class={className} data-slot="stat" {...rest}><slot /></div>
---
// src/components/ui/display/stat/StatDelta.astro — StatGroup compound part (see ../../README.md).
// The change beside a KPI: "12.4%" with a direction.
//
// THE DIRECTION IS A WORD, not a colour. Green-up / red-down is the canonical WCAG 1.4.1 failure —
// it is the only carrier of the meaning, it is invisible to a screen reader, and it is the pair of
// hues most people with colour-vision deficiency cannot separate. So this renders the direction as
// visually-hidden text ("Increased 12.4%"), and `data-trend` is what the theme colours and points an
// arrow with. Both channels, one source.
//
// Render it INSIDE StatValue's <dd>, not between two of them: a bare <span> is not a legal child of
// a <dl>, and dropping one there costs the whole list its term/value association.
//
// `label` replaces the English word, for a localised app or for a different vocabulary ("Up",
// "Rose", "+"). The number itself is the slot, so it is yours to format.
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"span"> & {
trend?: "up" | "down" | "flat";
/** Overrides the announced direction word. Defaults to Increased / Decreased / No change. */
label?: string;
};
const { trend = "flat", label, class: className, ...rest } = Astro.props;
const WORD = { up: "Increased", down: "Decreased", flat: "No change" } as const;
// The trailing space is deliberate: Astro 7 strips JSX-style whitespace between elements, so without
// it the announcement runs together as "Increased12.4%".
const direction = `${label ?? WORD[trend]} `;
---
<span class={className} data-slot="stat-delta" data-trend={trend} {...rest}>
<span data-slot="stat-delta-direction">{direction}</span><slot />
</span>
---
// src/components/ui/display/stat/StatGroup.astro — headless primitive (see ../../README.md).
// The KPI row every dashboard opens with — and it is a <dl>, not a row of <div>s.
//
// That is the whole reason this group exists: a stat is a name and a value, which is precisely what
// a description list is for. Written as divs, "Revenue" and "$12,480" are two unrelated strings that
// happen to sit near each other, and a screen reader user gets a wall of numbers with no way to tell
// which label owns which. Written as a <dl>, the browser carries the association for free.
//
// <StatGroup>
// <Stat>
// <StatLabel>Revenue</StatLabel>
// <StatValue>$12,480<StatDelta trend="up">12.4%</StatDelta></StatValue>
// </Stat>
// </StatGroup>
//
// `columns` is a hint the theme reads (`[data-columns="3"]`), not a grid — this library ships no
// layout. See DescriptionList for the content model both share.
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"dl"> & { columns?: 1 | 2 | 3 | 4 };
const { columns = 3, class: className, ...rest } = Astro.props;
---
<dl class={className} data-slot="stat-group" data-columns={columns} {...rest}>
<slot />
</dl>
---
// src/components/ui/display/stat/StatLabel.astro — StatGroup compound part (see ../../README.md).
// The KPI's name, as the <dt> of the pair. Put it BEFORE its StatValue: the association in a
// description list is positional, and a theme that wants the label under the number should move it
// with CSS rather than in the markup.
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"dt">;
const { class: className, ...rest } = Astro.props;
---
<dt class={className} data-slot="stat-label" {...rest}><slot /></dt>
---
// src/components/ui/display/stat/StatValue.astro — StatGroup compound part (see ../../README.md).
// The number, as the <dd>. Format it yourself — `Intl.NumberFormat` is the platform's answer and
// this library is not going to wrap it.
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"dd">;
const { class: className, ...rest } = Astro.props;
---
<dd class={className} data-slot="stat-value" {...rest}><slot /></dd>
import Stat from "./Stat.astro";
import StatDelta from "./StatDelta.astro";
import StatGroup from "./StatGroup.astro";
import StatLabel from "./StatLabel.astro";
import StatValue from "./StatValue.astro";
export { Stat, StatDelta, StatGroup, StatLabel, StatValue };
export default StatGroup;
What you get
The component source, copied into your project by npx astrocraft-ui add display/stat — no package dependency, no CSS to fight. Whatever the paid blocks compose, this is it.