Explainable change
A React component in Beamline's Dashboard category.
A metric whose change explains itself: at rest the value, the change against a named period and a thin ribbon of its causes under the change; opened in place, a bridge walks from the last period's value to this one with one column per cause, whose points add up exactly to the printed change; whatever the causes don't cover is drawn as Not explained, a cut axis says where it starts, and each cause opens the rows behind it (customers, accounts, orders), biggest first.
Use it for
- A dashboard number whose change people act on: revenue, members, cancellations, orders, hours, costs.
- The host computes the causes (new, expansion, churn, price; or its own) and the rows behind each; the part checks they add up, draws the walk and opens the rows.
Not for
- A KPI with a trend and no causes → metric-card.
- Why a single total is this much (base, add-ons, tax) → explainable-total.
Anatomy
- rest: the label, the value (rolling digits), the change chip with an arrow and a tone, the ribbon of causes under it, vs <previous period>, Why?
- sheet: <previous period> <value> → <period> <value> and Close; the bridge: the start column, a column per cause (points above, name and amount below, a dashed joint from the last column's end), Not explained when the causes fall short, the end column; Axis starts at … when cut
- cause rows: the cause's name, amount and count, its reason, then its rows biggest first with a bar of their share, amount and detail, and N more
States
- rest
- open
- cause chosen
- causes loading (shaped placeholders)
- rows loading
- not explained remainder
- cut axis
- down is good (invert)
- no change (says No change, does not open)
- no ratio (previous 0: units only)
- nothing to compare
- not measured
- error (Try again)
Keyboard
- Enter or Space on the metric opens it; Escape or Close folds it back with focus on the metric
- Tab reaches the causes as one stop; ←/→ (mirrored in RTL), Home and End move between them; Enter or Space opens a cause's rows (again closes them); Escape closes the rows with focus back on the cause
- Each cause reads its name, amount, its points of the change and its count; the rows are a polite live region
Motion
The metric unfolds in place (the shared height morph); each column grows out of its ribbon segment, one after another, then the joints and points arrive. A chosen cause's rows slide in, biggest first; the others step back. Folding the bridge drops the chosen cause, so the next opening starts from the whole walk. Bars are lit from above (a lighter top edge, a hairline of light). New values move the columns and roll the digits. Nothing runs at rest; reduced motion places everything.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label (required) | string | — | What the metric is: "Monthly recurring revenue". |
value (required) | number | null | — | This period's value; null when it is not measured (shown as such, never as 0, and nothing opens). |
previous | number | null | — | The value it is compared with. Without it there is no change to explain; at 0 the change shows in units only (a percentage of nothing is refused). |
period | string | — | This period's name: "October". |
previousPeriod | string | — | The compared period's name: "September". It is named beside the change ("vs September"). |
causes | ChangeCause[] | — | The causes of the change, computed by the host; undefined while it works them out (placeholders of their shape show). Whatever they don't add up to is drawn as "Not explained", never left out or spread over the others. |
unexplainedReason | ReactNode | — | Why part of the change has no cause, when the host knows: "refunds from 30 Oct not matched yet". |
format | ((value: number) => string) | — | How the value and every amount are written. Default: the locale's number format with `formatOptions`. |
formatOptions | NumberFormatOptions | — | Intl options for the default format: `{ style: "currency", currency: "USD", maximumFractionDigits: 0 }`. |
invert | boolean | — | Down is good (churn, reply time): a fall, and the causes that lower it, read in the good tone. |
status | "error" | "ready" | — | "error" when the causes can't be had: the value and change stay, the bridge says so. |
onRetry | (() => void) | — | |
onCauseOpen | ((causeId: string) => void) | — | A cause was opened (by click, Enter or Space): fetch its rows here when they aren't sent with the cause. |
onRowOpen | ((row: ChangeRow, cause: ChangeCause) => void) | — | A row without `href` was chosen. |
cause | string | null | — | Controlled: the cause whose rows are open; null for none. |
defaultCause | string | null | — | |
onCauseChange | ((causeId: string | null) => void) | — | |
open | boolean | — | Controlled: whether the bridge shows; omit to use defaultOpen. |
defaultOpen | boolean | — | Initial open state when uncontrolled; defaults to false. |
onOpenChange | ((open: boolean) => void) | — | Reports an open-state change; no callback by default. |
className | string | — | Classes on the documented visual root; no override by default. |
classNames | Partial<Record<"root" | "column" | "trigger" | "rows" | "sheet" | "bridge", string>> | — | Classes for the named visual parts; no overrides by default. |
style | CSSProperties | — | Inline styles on the documented visual root; no overrides by default. |
ref | Ref<HTMLDivElement> | — | Receives the documented native target or imperative handle; omitted by default. |
Built from
Dependencies
lucide-react
Import
import { ExplainableChange } from "@beamline/explainable-change";
Get it
Explainable change is part of Beamline UI. Install the parts you need in your React app as compiled packages or editable source. Get Beamline · Connect your agent
More in Dashboard
- Explainable total: A total that answers "why this much?" in place: it unfolds into the steps that made it — a waterfall whose bars start where the last step ended, each…
- Budget vessels: Budgets as glasses of money: the liquid is what is left (free money on top, committed money as a lighter layer under it), the lip at the period's budget…
- Insight cards: Findings from an agent, one per page: the sentence, the series it is about with their change, a chart to scrub that names each point, and a follow-up…
- Tracker: A row of status blocks for uptime, job runs or daily checks, with tooltips and a legend.
- Metric card: A KPI that fills its tile: label, a value that rolls to each new number, the change with arrow, sign and tone, what it compares with and the value it…
- Category bar: One bar split into labelled ranges with an optional marker that names its range.
- Bar list: Ranked horizontal bars, label and value over a meter, one line per row when there is room — top pages, sources, customers.
- App shell: The frame for every app screen: collapsible sidebar with grouped navigation, top bar and page.