Skip to main content
astrocraft-ui/ components · 101

Stat

Free · MIT

Headless, 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

Install command
npx astrocraft-ui add display/stat

Plain-CSS theme — no build step

Install command
npx astrocraft-ui add display/stat --theme css

Tailwind theme — needs Tailwind v4

Install command
npx astrocraft-ui add display/stat --theme tailwind

Plain-CSS theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add display/stat --theme css --bridge lumos

Tailwind theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add display/stat --theme tailwind --bridge lumos

Live 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.

ComponentSlotsVariantsRuntime stateNative 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
---
// 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
---
// 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
---
// 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
---
// 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
---
// 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>
src/components/ui/display/stat/index.ts
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.