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

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

PropTypeDefaultDescription
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).
previousnumber | 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).
periodstring—This period's name: "October".
previousPeriodstring—The compared period's name: "September". It is named beside the change ("vs September").
causesChangeCause[]—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.
unexplainedReasonReactNode—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`.
formatOptionsNumberFormatOptions—Intl options for the default format: `{ style: "currency", currency: "USD", maximumFractionDigits: 0 }`.
invertboolean—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.
causestring | null—Controlled: the cause whose rows are open; null for none.
defaultCausestring | null—
onCauseChange((causeId: string | null) => void)—
openboolean—Controlled: whether the bridge shows; omit to use defaultOpen.
defaultOpenboolean—Initial open state when uncontrolled; defaults to false.
onOpenChange((open: boolean) => void)—Reports an open-state change; no callback by default.
classNamestring—Classes on the documented visual root; no override by default.
classNamesPartial<Record<"root" | "column" | "trigger" | "rows" | "sheet" | "bridge", string>>—Classes for the named visual parts; no overrides by default.
styleCSSProperties—Inline styles on the documented visual root; no overrides by default.
refRef<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.

Part of Beamline UI: polished, editable React components and complete screens for custom applications. Build your own style from any look; tokens, fonts, radii, motion; not only colours.