Feedback
Statuses and freshness
A state as a word and a colour, with the state itself as the source of the colour — and the age of the data beside it, because in a data product “how old is this?” is part of the number's meaning.
Examples
The vocabulary
Seven presets, one meaning each. The word is the accessible carrier and the colour is the fast one; extending a status means extending the vocabulary, not the palette.
| Status | Tone | Meaning |
|---|---|---|
| Live | positive + pulse | Receiving data right now. The dot pulses; use it rarely. |
| Healthy | positive | Fresh and within expectations. |
| Degraded | warning | Working, but worse than expected. Still usable. |
| Stale | warning | Data is older than the freshness window. |
| Pending | info | In progress; no verdict yet. Not a problem. |
| Down | negative | Failed or unavailable. Needs attention. |
| Unknown | neutral | No signal. Never render this as an error. |
<StatusBadge status="live" />
<StatusBadge status="healthy" />
<StatusBadge status="degraded" />
<StatusBadge status="pending" />
<StatusBadge status="stale" />
<StatusBadge status="down" />
<StatusBadge status="unknown" />A custom label, an unchanged colour
The preset owns the tone and the dot; `label` overrides the word. A product that needs “Healthy · 3 regions” says so beside the status it already has rather than inventing a colour for it.
<StatusBadge status="healthy" label="Healthy · 3 regions" />
<StatusBadge status="live" size="sm" label="Streaming" />Freshness
Thresholds are explicit props, so “stale” is never a mystery, and the age is computed against an injected clock — which is what makes the server and the browser agree on the same string.
8 seconds ago
Live · 8s ago3 minutes ago
Fresh · 3m ago42 minutes ago
Stale · 42m ago6 hours ago
Outdated · 6h ago<FreshnessBadge updatedAt={updatedAt} freshSeconds={300} staleSeconds={3600} showAge />Timestamps
Relative by default, exact on hover, and the machine-readable value is always in the DOM. The absolute form takes a `timeZone`, because a data product's readers are not all in one.
relative
absolute (UTC)
<Timestamp value={ago(30)} now={NOW} />
<Timestamp value={publishedAt} display="absolute" style="datetime" timeZone="UTC" />In a data quality panel
How the three combine with row counts in a real surface: the word, the age, and a note about why a table is in the state it is in.
market_candles_1hLive1,284,020provider: binancemarket_candles_1dHealthy18,420provider: binancemarket_candles_1wHealthy2,640provider: binancelegacy market_candlesStale—compatibility onlyretained parquet snapshotPendingretainedreader migration pending<StatusBadge status="stale" size="sm" />
<Timestamp value={row.updated} now={NOW} />Usage
import { StatusBadge } from "@luc/ui/data";<StatusBadge status="degraded" />The preset owns the label, the tone and whether the dot pulses; `label` overrides the word and never the colour.
API
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
The table above is `StatusBadge`'s props; `statusPresets` is the vocabulary it resolves against, and a product extends it through `status` plus `label` rather than by adding colours.
| Export | Kind | Signature |
|---|---|---|
StatusBadge | 3 documented props | ({ className, status, label, size, ...props }: StatusBadgeProps) => import("@types/react").JSX.Element |
statusPresets | export | { readonly live: { readonly tone: "positive"; readonly label: "Live"; readonly pulse: true; }; readonly healthy: { readonly tone: "positive"; readonly label: "Healthy"; }; readonly positive: { readon… |
StatusBadge props
Type: StatusBadgeProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
label? | string | undefined | — | Override the preset label. |
size? | "sm" | "md" | undefined | — | |
status | "pending" | "positive" | "negative" | "down" | "live" | "healthy" | "degraded" | "stale" … | — |
FreshnessBadge and Timestamp
Both live in their own module and have their own page, with the compiled tables for every prop — including `now`, `locale` and `timeZone`. Open the Timestamp page.
Guidelines
Do
- Use a preset wherever the vocabulary already covers the state — the colour is then decided once, in the library.
- Reserve `live` for a state that really is being measured right now; a pulsing dot on a stale value is a lie.
- Pair a freshness label with an absolute timestamp somewhere in reach, because "4m" becomes wrong as the page sits open.
Don't
- Do not let colour carry the status alone — the word is both the accessible and the colour-blind-safe carrier.
- Do not invent a tone for a status the presets do not have; extend the vocabulary rather than the palette.
- Do not attach a freshness badge to data that is refreshed on every render.
Accessibility
The badge reads as its label, so the status is announced as a word rather than as a colour. The dot is decorative and `aria-hidden`, and `Live`'s pulse is a slow opacity change that stops under `prefers-reduced-motion` — it never travels. Nothing here is a live region: a status that changes on its own should be announced by the region that owns it, not by every badge on the page.
Server and browser agree
A relative time is computed from a clock, and two clocks are two strings: without an injected `now`, a page rendered at 12:00:59 and hydrated at 12:01:00 disagrees about “1m” and React reports a hydration mismatch. Every specimen above passes one fixed clock, which is what a product should do for anything server-rendered.
Related