Scaled Bar Group

A group of horizontal bars scaled to a shared maximum, so a set of pots reads by relative size.

  • Safety net
    need £25k
    In placehave £25k today
  • Home deposit
    need £100k
    £50k shorthave £50k today
  • Early-retirement pot
    need £441k
    £383k shorthave £58k today
  • Pension
    need £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 net
    need £25k
    In placehave £25k today
  • Home deposit
    need £100k
    £50k shorthave £50k today
  • Early-retirement pot
    need £441k
    £383k shorthave £58k today
  • Pension
    need £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

PropTypeDefaultDescription
rowsScaledBarRow[]Required. Rows top to bottom. Pass a stable reference.
maxnumberlargest row totalThe value mapped to a full-width bar.
legendScaledBarLegendItem[]Shared legend rendered once beneath every row.
heightnumber24Bar height in px.
minBarWidthnumber8Minimum rendered bar width in px so tiny rows stay visible.
labelWidthstring"10rem"Width of the label column (any CSS length).
classNamestringSizing / spacing hook.

ScaledBarRow

FieldTypeDefaultDescription
labelReact.ReactNodeRequired. Left-column label.
segmentsStackedBarSegment[]Required. Segments driving the bar.
totalnumbersum of segment valuesThe value the bar width is measured against max.
trailingReact.ReactNodeInline node to the right of the bar (e.g. a total).
captionReact.ReactNodeNode below the bar (e.g. a status line).
barLabelstringAccessible label for the row's bar (sets role="img").
keystringarray indexStable 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.

OptionTypeDefaultDescription
valuenumberRequired. Amount held today.
targetnumberRequired. Amount required.
metColorstringvar(--chart-positive-fill)Fill when value >= target.
shortColorstringvar(--chart-primary-fill)Fill when short of the target.
trackColorstringvar(--neutral-subtle)Faint remainder colour.
haveLabelstring"Have"Legend/tooltip label for the filled segment.
remainderLabelstring"Still needed"Legend/tooltip label for the remainder.
formatValue(amount: number) => stringFormats each segment's amount for the legend + tooltip (e.g. a currency formatter).

On this page