import { BigNumber } from "semiotic/value"
Quick Start
BigNumber is the catalog's answer to“I have one number — show me the number.” It ships undersemiotic/value as a plain React component (no Stream Frame) so it renders cleanly in SSR, embeds in prose, and comes in at about 6 KB gz.
The component is intentionally feature-rich — it's the forward-looking POC for a futureSingleValueFrame: every contract that frame would own (format cascade, threshold zones via semantic theme roles, comparison anchoring, sentence-form ARIA, push API with staleness) is already wired here.
Q3 RevenueYear-to-date bookings
$1,284,900
+$304,900(+31.1%)vs Q286%of $1,500,000 Q3 plan
TSX
import { BigNumber } from "semiotic/value" import { LineChart } from "semiotic/xy" <BigNumber value={1284900} label="Q3 Revenue" caption="Year-to-date bookings" format="currency" precision={0} comparison={{ value: 980000, label: "vs Q2" }} target={{ value: 1500000, label: "Q3 plan" }} thresholds={[ { at: -Infinity, level: "danger" }, { at: 1000000, level: "warning" }, { at: 1300000, level: "success" }, ]} trendSlot={(ctx) => ( <LineChart data={recentMonths.map((y, x) => ({ x, y }))} xAccessor="x" yAccessor="y" mode="sparkline" width={328} height={36} color={ctx.color} /> )} />
Layout Modes
Four modes wrap the focal value in different chrome envelopes. The same content reformats per mode — useful for dashboards (tile), hero cards (presentation), in-paragraph numbers (inline), and dense grids (thumbnail).
Tile modedashboard cell
72%
Inline mode flows in prose:32%+4(+14.3%) — handy for stat-driven copy that needs sentiment colour without a card.
Uptime this quarterpresentation mode
94%
+2(+2.4%)vs last quarter
Threshold Zones
Threshold zones map a value into a status level (success, warning,danger, info, neutral). The resolved level paints the focal value via the matching --semiotic-{level} CSS variable, so theming flows through without per-card colour overrides.
Built-in shortcuts (number, currency, percent,compact, duration) use Intl.NumberFormat with yourlocale + currency + precision props. Pass a function for anything else.
Comparison + Target
Pair the focal value with a prior-period comparison or a goal. Sentiment is inferred fromdirection (default “higher-is-better”) and the sign of the delta — override with the sentiment prop when the rule doesn't hold (e.g. P99 latency where lower is better).
Higher is better
$1,284,900
+$304,900(+31.1%)vs Q2
P99 latencylower is better → red is bad
486 ms
+76(+18.5%)vs last week
of target
750
75%of 1,000 Q3 goal
Embedded Charts (Two Slots)
BigNumber ships no built-in chart renderer. The card is a layout host: the consumer composes their own Semiotic chart (or any ReactNode) into one of two slots, picked by the chart's aspect ratio.
trendSlot — wide / rectangular charts beneath the value. LineChart, AreaChart,DifferenceChart, anything that reads as a horizontal strip. mode="sparkline" on the chart strips chrome automatically.chartSlot — square charts anchored top-right. DonutChart, PieChart,Scatterplot, Treemap, CirclePack— anything that wants equal width and height. Rendered at sparkline scale (~two line-heights) so the chart reads as a decoration beside the focal value, not a competitor.
Both slots accept a ReactNode or a(ctx) => ReactNode. The function form receives a slot context with the resolved threshold colour (ctx.color), the threshold level (ctx.level), sentiment, formatted value, and the live pushBuffer — so the embedded chart can theme-link to the focal value without prop-drilling, and re-render alongside live updates.
trendSlot: wide / rectangular charts
LineChart slot
$1,284,900
+$304,900(+31.1%)vs Q2
AreaChart slot
94 %
+2(+2.4%)vs Q2
TSX
import { BigNumber } from "semiotic/value" import { LineChart } from "semiotic/xy" <BigNumber value={1284900} label="Q3 Revenue" format="currency" trendSlot={(ctx) => ( <LineChart data={recentMonths.map((y, x) => ({ x, y }))} xAccessor="x" yAccessor="y" mode="sparkline" width={260} height={32} color={ctx.color} // resolves to the threshold's --semiotic-{level} /> )} />
chartSlot: square charts (top-right corner)
Pass a square-aspect chart and BigNumber anchors it in the top-right corner of the card at sparkline scale — roughly two line-heights, so it reads as a decorative spark next to the focal value rather than competing with it. The reserved square defaults to 44px (tile) / 80px (presentation); override withchartSize.
Revenue mixby region
$1,284,900
Survey responsespositive sentiment
42 %
+4(+10.5%)vs last month
Correlationprice ↔ demand
0.71
TSX
import { BigNumber } from "semiotic/value" import { DonutChart } from "semiotic/ordinal" <BigNumber value={1284900} label="Revenue mix" format="currency" chartSlot={ <DonutChart data={revenueByRegion} categoryAccessor="region" valueAccessor="revenue" mode="sparkline" width={44} height={44} innerRadius={9} margin={0} /> } />
Both slots at once
Use trendSlot + chartSlot together when both a time-series and a part-to-whole view make sense — the square chart anchors the top-right, the wide trend stretches across the bottom.
Q3 Revenueby region + 6-month trajectory
$1,284,900
+$304,900(+31.1%)vs Q2
Streaming (push API + staleness)
Omit the value prop semantics and feed live updates through a ref handle:push / pushMany / clear / getValue /getData. The auto-trend buffer is bounded by windowSize and the card dims itself when no push lands within stalenessThreshold.
Live BookingsUpdated every 700ms
$0
−$1,000,000(-100%)vs plan0%of $1,500,000 Q3 plan
TSX
function LiveBookings() { const ref = useRef<BigNumberHandle>(null) useEffect(() => { const id = setInterval(() => { ref.current?.push({ value: nextSample(), time: Date.now() }) }, 700) return () => clearInterval(id) }, []) return ( <BigNumber ref={ref} value={0} // initial / fallback label="Live Bookings" format="currency" precision={0} windowSize={40} // bounded trend buffer stalenessThreshold={2500} // dim after 2.5s of no push animate={{ duration: 220 }} comparison={{ value: 1000000, label: "vs plan" }} target={{ value: 1500000, label: "Q3 plan" }} thresholds={[ { at: -Infinity, level: "danger" }, { at: 1000000, level: "warning" }, { at: 1300000, level: "success" }, ]} /> ) }
Slot Overrides
Every layout slot — headerSlot, valueSlot,deltaSlot, trendSlot (wide chart),chartSlot (square chart), footerSlot — accepts a ReactNode or a function receiving the resolved context (formatted value, delta, level, color, sentiment, isStale, pushBuffer). Use these to plug your own chart in or to fully replace a sub-region without losing the formatting / threshold cascade.
TSX
<BigNumber value={1284900} label="Revenue" format="currency" comparison={{ value: 980000 }} valueSlot={(ctx) => ( <div style={{ fontSize: 72, color: ctx.sentiment === "positive" ? "var(--semiotic-success)" : "var(--semiotic-danger)" }}> {ctx.formattedValue} </div> )} footerSlot={(ctx) => ctx.isStale ? <em>data is {ctx.delta} stale</em> : null} />
Theming
Every visible piece of BigNumber reads from theme CSS variables: the focal value picks its colour from --semiotic-{level} via the resolved threshold, the card chrome reads from--semiotic-border / --semiotic-surface /--semiotic-border-radius, the trend sparkline inherits the value colour and renders through Semiotic's LineChart inmode="sparkline", and the typography flows through the theme's --semiotic-font-family. Wrap the card (or any ancestor) in ThemeProvider and every part updates in lockstep:
Q3 RevenueYear-to-date bookings
$1,284,900
+$304,900(+31.1%)vs Q286%of $1,500,000 Q3 plan
P99 latencyvalue rose — danger because lower-is-better
486 ms
+76(+18.5%)vs last week
Theming works withoutThemeProvider too — set the CSS variables on any ancestor (a section, a dark-mode media query, a forced-colors block) and the card picks them up via standard CSS cascade. That's how the sparkline inside a BigNumber ends up matching a Tufte-themed bar chart sitting beside it.
Semantic CSS Classes
Every meaningful element carries a BEM-style class so you can target individual parts (or whole-card states like sentiment / level / stale) from a stylesheet — no inline-style overrides, no framePropsgymnastics. The class names mirror the data attributes for consistency.
TEXT
semiotic-bignumber (root) semiotic-bignumber--mode-{tile|presentation|inline|thumbnail} semiotic-bignumber--level-{success|warning|danger|info|neutral} semiotic-bignumber--sentiment-{positive|negative|neutral} semiotic-bignumber--stale (only when stalenessThreshold fires) semiotic-bignumber--loading (only during loading state) semiotic-bignumber--empty (only when value is empty) semiotic-bignumber__top (only when chartSlot is set — row layout) semiotic-bignumber__text-region semiotic-bignumber__header semiotic-bignumber__label semiotic-bignumber__caption semiotic-bignumber__value semiotic-bignumber__value--{level} semiotic-bignumber__value-text semiotic-bignumber__unit semiotic-bignumber__delta semiotic-bignumber__delta--{up|down|flat} semiotic-bignumber__delta-row semiotic-bignumber__delta-row--{up|down|flat} semiotic-bignumber__arrow semiotic-bignumber__arrow--{up|down|flat} semiotic-bignumber__delta-amount semiotic-bignumber__delta-percent semiotic-bignumber__comparison-label semiotic-bignumber__target semiotic-bignumber__target-percent semiotic-bignumber__target-value semiotic-bignumber__chart (square chart slot, beside the value) semiotic-bignumber__trend (wide chart slot, beneath the value) semiotic-bignumber__footer
Example — restyle the up / down indicators on a dashboard-wide basis:
CSS
.semiotic-bignumber__arrow { font-size: 0.75em; margin-right: 2px; } .semiotic-bignumber__arrow--up { color: oklch(70% 0.18 145); } .semiotic-bignumber__arrow--down { color: oklch(60% 0.20 25); } /* Use cascade modifiers to overrule the auto sentiment colour on a single tile */ .kpi-strict .semiotic-bignumber--sentiment-positive .semiotic-bignumber__delta-row { color: var(--semiotic-info); } /* Suppress the trend sparkline on print without re-rendering the React tree */ @media print { .semiotic-bignumber__trend { display: none; } }
Accessibility
The outer container renders a sentence-form aria-label that bundles every piece of the displayed value: label, formatted value, unit, delta with direction word, percent change, comparison label, target percent, and a stale tag when staleness fires. Pass description to override entirely, or summary to add a screen-reader-only supplement below.
Threshold zones use semantic theme roles, so a high-contrast or forced-colors theme automatically remaps the value text colour without per-card overrides.
Props
Core props are listed below. BigNumber also accepts slot overrides (headerSlot, valueSlot,deltaSlot, footerSlot) and layout props (align, padding, emphasis,color, background, borderColor,borderRadius), each a ReactNode or(ctx) => ReactNode.
| Prop | Type | Required | Default | Description |
|---|
value | number | Yes | — | The focal value to display. |
label | string | — | — | Short caption above the value. |
caption | string | — | — | Secondary caption below the value. |
format | "number" | "currency" | "percent" | "compact" | "duration" | function | — | "number" | Value formatter; pass a function for full control. |
locale | string | — | — | BCP-47 locale for number/currency formatting. |
currency | string | — | — | ISO currency code when format is "currency". |
precision | number | — | — | Fraction-digit count for the formatted value. |
prefix | string | — | — | Text rendered before the value. |
suffix | string | — | — | Text rendered after the value. |
unit | string | — | — | Unit appended to the value and the aria-label. |
comparison | object | — | — | { value, label?, format?, direction? } — drives the delta row. |
target | object | — | — | { value, label?, format?, direction? } — drives the % -of-target readout. |
delta | number | — | — | Explicit delta override (otherwise derived from comparison). |
showDeltaPercent | boolean | — | true | Show the percent change alongside the signed delta. |
direction | "higher-is-better" | "lower-is-better" | "neutral" | — | "higher-is-better" | How delta sign maps to sentiment. |
sentiment | "auto" | "positive" | "negative" | "neutral" | — | "auto" | Force the sentiment color instead of deriving it. |
thresholds | array | — | — | [{ at, level, color?, label? }] — resolved by highest at ≤ value, painted via --semiotic-{level}. |
mode | "tile" | "presentation" | "inline" | "thumbnail" | — | "tile" | Layout/typography preset. |
windowSize | number | — | 60 | Caps the push buffer surfaced via getData() / slot context. |
animate | boolean | object | — | — | Tween between value changes ({ duration?, easing?, intro? }). |
stalenessThreshold | number | — | — | Milliseconds without a push after which the card dims. |
staleLabel | string | — | — | Label shown when the value is stale. |
trendSlot | ReactNode | function | — | — | Wide chart slot beneath the value (sparkline-aspect charts). |
chartSlot | ReactNode | function | — | — | Square chart slot beside the value (donut/pie/scatter). |
chartSize | number | — | — | Pixels reserved for chartSlot; defaults to 44px in tile mode and 80px in presentation mode. |
Future SingleValueFrame
BigNumber ships today as plain React because the streaming / threshold / format contracts justify the component-level investment but don't yet justify theframe-level investment (a new frame is ~1–2k LOC of hit-tester / scene-to-SVG / SSR / theming scaffolding). The roadmap reserves aSingleValueFrame for the day live-KPI adoption gives the frame fixed cost an obvious payoff — at which point this component re-anchors on top of it without API change.