Segmented bar
A distribution on one 8px track, with an optional legend of counts and shares.
Segmented bar shows how a whole splits into parts, like where a campaign's recipients ended
up. It is the multi-value sibling of Progress
size="sm": the same 8px track, with each segment its own rounded pill, 1px from the next.
Each segment takes a status tone or a category color, and with legend a list under the
bar names every part with its count and share. Pointing at a segment highlights its legend
row, and pointing at or focusing a row dims the rest of the bar.
- Delivered1,14895%
- Deferred00%
- Bounced413%
- Not sent151%
Usage
import { SegmentedBar } from '@/components/ui/segmented-bar';
<SegmentedBar
label="Delivery"
total={recipients}
legend
segments={[
{ label: 'Delivered', value: delivered, tone: 'success' },
{ label: 'Bounced', value: bounced, tone: 'destructive' },
{ label: 'Not sent', value: notSent },
]}
/>Total and bare track
total sets what the widths and shares are out of. It defaults to the sum of the segments,
and a larger total leaves bare track, like the recipients still queued while a campaign
sends. A total smaller than the sum is raised to the sum, so the segments never overflow the
track. Switch between the two snapshots above to see the segments ease to their new widths.
Segments with a value of zero or less are left off the bar and stay in the legend, and a
segment that is a sliver of the total is drawn at least 2px wide so it can still be seen.
Shares round to whole percents, and a part too small to round up reads as <1% rather than
0%.
Colors
Tones are status roles: success, info, warning, destructive, and neutral, the
default. Each status segment sets the matching tone, where
destructive is the danger tone, and neutral is a quiet gray with no tone. When the parts are categories rather than outcomes, give each a color from the
category palette instead, which wins over
tone. The colors are gray, red, orange, amber, yellow, green, blue, purple, and
pink.
Opens by client
With or without a legend
With legend, a row under the bar lists each part with its swatch, label, count, and share.
Without it, the bar stands alone and its name carries every part, which suits a row where
the figures are already written beside it.
Motion
| Rule | What happens |
|---|---|
| Values change | Segments ease to their new widths on --duration-base and --ease-out. |
| Pointing or focus | The other segments drop to --alpha-32, and the matching legend row fills. |
| Empty part | A zero leaves the bar and stays in the legend at 0, so the list does not reshuffle. |
| Reduced motion | Widths and dimming change without easing. |
Props
Segmented bar takes every <div> attribute except children, plus these.
| Prop | Type | Default | What it does |
|---|---|---|---|
segments | { label, value, tone?, color? }[] | The parts, left to right. tone is a status role and color a category hue, which wins over tone. | |
total | number | The sum | What widths and shares are out of. A larger total leaves bare track. |
legend | boolean | false | Lists each segment under the bar with its count and share. |
label | string | Names the bar, or the legend when there is one. | |
formatValue | (value: number) => string | toLocaleString | Formats counts in the legend and in the accessible summary. |
Accessibility
- Without a legend, the bar is
role="img"with anaria-labelthat summarizes every part, like "Delivery: Delivered 1,148 (95%), Bounced 41 (3%)". The label comes first when you pass one. - With a legend, the bar is hidden from assistive tech and the legend, a list named by
label, carries the same facts as text. Give it alabel, because a legend with none has no name. - Counts and shares are always text, so color never carries a part on its own. The legend swatches are decorative, and the label beside each says what it is.
- Each legend row takes focus, with a 2px ring, and focusing one highlights its segment the way pointing does. A part with a value of zero has no segment, so the whole bar dims. The segments themselves are not focusable.
- Changes are not announced, so a bar that updates while a person watches needs a live region of your own.
- Under reduced motion widths and dimming change without easing, and the legend's fill changes without a transition.
- In forced colors every segment and swatch draws in
CanvasText, split by its 1px gaps, inside a bordered track, so the bar still shows the proportions while the summary or the legend names each part.
When to use something else
- Progress for one value moving toward a total, like an upload or an import. It is a progressbar that announces its value, and a segmented bar is a snapshot of parts.
- Stat strip when the exact numbers are the point and the split is secondary.
- Sparkline for a trend over time, not a split at one moment.
- A charting library for many parts, axes, or values that need hover detail. Gesso has no chart item.
Install
shadcn add @gesso/segmented-bar