Scaled Bar Group
A group of horizontal bars scaled to a shared maximum, so a set of pots reads by relative size.
- Safety netneed £25kIn placehave £25k today
- Home depositneed £100k£50k shorthave £50k today
- Early-retirement potneed £441k£383k shorthave £58k today
- Pensionneed £1.3m£1.17m shorthave £127k today
Usage
import { ScaledBarGroup, targetSegments } from "@reva/ui";
<ScaledBarGroup
rows={[
{
label: "Safety net",
total: 25000,
segments: targetSegments({ value: 25000, target: 25000 }),
trailing: "need £25k",
},
{
label: "Home deposit",
total: 100000,
segments: targetSegments({ value: 50000, target: 100000 }),
trailing: "need £100k",
},
]}
/>Each row is a StackedBar (its legend suppressed) inside a width-scaled wrapper. A row's width is its total measured against the group max (the largest total by default), so the biggest pot fills the track and the rest shrink against the same reference — the bars compare by size, not just internal proportion. Pure flexbox, no plotting library.
Two shapes fall out of this: a progress view (a filled part plus a faint remainder, via targetSegments) and a breakdown view (multi-segment bars that sum to a total, plus a shared legend).
Examples
Have vs required
Build each row with targetSegments({ value, target }) and set the row total to the target. The fill reads green when the target is met and rust when short, with a faint remainder for what is still needed. A caption carries the status line beneath the bar.
- Safety netneed £25kIn placehave £25k today
- Home depositneed £100k£50k shorthave £50k today
- Early-retirement potneed £441k£383k shorthave £58k today
- Pensionneed £1.3m£1.17m shorthave £127k today
import { ScaledBarGroup, Text, targetSegments } from "@reva/ui";const pots = [{ name: "Safety net", have: 25000, target: 25000 },{ name: "Home deposit", have: 50000, target: 100000 },{ name: "Early-retirement pot", have: 58000, target: 441000 },{ name: "Pension", have: 127000, target: 1300000 },];<ScaledBarGroupheight={20}rows={pots.map((pot) => ({ label: pot.name, total: pot.target, segments: targetSegments({ value: pot.have, target: pot.target }), trailing: `need £${pot.target}`, caption: ( <Text as="span" size="sm" color="muted-foreground"> have today </Text> ),}))}/>Funding each pot
Pass multi-segment rows that sum to each pot's total, plus a shared legend rendered once beneath every row. The segments carry meaning, so they take chart role tokens (--chart-neutral-fill, --chart-primary-fill, --chart-accent-fill).
- Home deposit£100k
- Early retirement£441k
- Pension£1.3m
- Have now
- Your contributions
- Growth
import { ScaledBarGroup } from "@reva/ui";<ScaledBarGroupheight={28}legend={[ { label: "Have now", color: "var(--chart-neutral-fill)" }, { label: "Your contributions", color: "var(--chart-primary-fill)" }, { label: "Growth", color: "var(--chart-accent-fill)" },]}rows={[ { label: "Home deposit", trailing: "£100k", segments: [ { label: "Have now", value: 50000, color: "var(--chart-neutral-fill)" }, { label: "Your contributions", value: 45000, color: "var(--chart-primary-fill)" }, { label: "Growth", value: 5000, color: "var(--chart-accent-fill)" }, ], }, { label: "Pension", trailing: "£1.3m", segments: [ { label: "Have now", value: 127000, color: "var(--chart-neutral-fill)" }, { label: "Your contributions", value: 715000, color: "var(--chart-primary-fill)" }, { label: "Growth", value: 458000, color: "var(--chart-accent-fill)" }, ], },]}/>Props
ScaledBarGroup
| Prop | Type | Default | Description |
|---|---|---|---|
rows | ScaledBarRow[] | — | Required. Rows top to bottom. Pass a stable reference. |
max | number | largest row total | The value mapped to a full-width bar. |
legend | ScaledBarLegendItem[] | — | Shared legend rendered once beneath every row. |
height | number | 24 | Bar height in px. |
minBarWidth | number | 8 | Minimum rendered bar width in px so tiny rows stay visible. |
labelWidth | string | "10rem" | Width of the label column (any CSS length). |
className | string | — | Sizing / spacing hook. |
ScaledBarRow
| Field | Type | Default | Description |
|---|---|---|---|
label | React.ReactNode | — | Required. Left-column label. |
segments | StackedBarSegment[] | — | Required. Segments driving the bar. |
total | number | sum of segment values | The value the bar width is measured against max. |
trailing | React.ReactNode | — | Inline node to the right of the bar (e.g. a total). |
caption | React.ReactNode | — | Node below the bar (e.g. a status line). |
barLabel | string | — | Accessible label for the row's bar (sets role="img"). |
key | string | array index | Stable React key. |
targetSegments
Builds the two segments for a have-vs-required bar: a filled part (green when met, rust when short) plus a faint remainder for what is still needed. Returns StackedBarSegment[] — feed it to a row with total set to the target.
| Option | Type | Default | Description |
|---|---|---|---|
value | number | — | Required. Amount held today. |
target | number | — | Required. Amount required. |
metColor | string | var(--chart-positive-fill) | Fill when value >= target. |
shortColor | string | var(--chart-primary-fill) | Fill when short of the target. |
trackColor | string | var(--neutral-subtle) | Faint remainder colour. |
haveLabel | string | "Have" | Legend/tooltip label for the filled segment. |
remainderLabel | string | "Still needed" | Legend/tooltip label for the remainder. |
formatValue | (amount: number) => string | — | Formats each segment's amount for the legend + tooltip (e.g. a currency formatter). |