One bar cut into shares with a legend that says the same in numbers. Pointing at a legend row lights its slice and dims the rest, and choosing it keeps it lit.
Where the spend goes
$15,760
"use client"
import { SplitBar } from "@/components/ballmac/split-bar"
export default function SplitBarDemo() {
return (
<div className="w-full max-w-lg">
<SplitBar
title="Where the spend goes"
total="$15,760"
format={(v) => `$${v.toLocaleString("en-US")}`}
segments={[
{ id: "compute", label: "Compute", value: 6120 },
{ id: "models", label: "Model calls", value: 4320 },
{ id: "db", label: "Databases", value: 2380 },
{ id: "cdn", label: "CDN", value: 1120 },
{ id: "logs", label: "Logs", value: 1450 },
{ id: "other", label: "Other", value: 370 },
]}
/>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/split-barAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/split-bar.tsx// Ballmac UI: Split Bar. https://ui.ballmac.com/components/split-bar "use client" import * as React from "react" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type SplitSegment = { id: string label: string value: number /** A note beside the value, for example "$1,200". */ hint?: string } type SplitBarProps = Omit<React.ComponentProps<"div">, "title"> & { segments: SplitSegment[] title?: string /** Formats each value. */ format?: (value: number) => string /** Shows a total above the bar. */ total?: string } const SHADES = ["bg-foreground", "bg-chart-1", "bg-chart-2", "bg-chart-3", "bg-chart-4", "bg-chart-5"] const nf = new Intl.NumberFormat("en-US") /** * One bar cut into shares, with a legend that says the same thing in numbers. Pointing at a legend row or focusing it lights up its slice and dims the rest, and choosing it keeps it lit; * the legend is the accessible version and carries the values and the percentages. */ function SplitBar({ segments, title, format = (v) => nf.format(v), total, className, ...props }: SplitBarProps) { const msg = useMessages() const id = React.useId() const [hover, setHover] = React.useState<string | null>(null) const [pinned, setPinned] = React.useState<string | null>(null) const active = hover ?? pinned const sum = segments.reduce((s, x) => s + x.value, 0) || 1 return ( <div data-slot="split-bar" className={cn("grid min-w-0 gap-3", className)} {...props}> {(title || total) && ( <div className="flex items-baseline justify-between gap-3"> {title && <h3 id={`${id}-t`} className="text-sm font-medium">{title}</h3>} {total && <p className="text-sm font-semibold tabular-nums">{total}</p>} </div> )} <div aria-hidden="true" className="flex h-3 gap-0.5 overflow-hidden rounded-full"> {segments.map((s, i) => ( <span key={s.id} className={cn("min-w-1 transition-opacity motion-reduce:transition-none", SHADES[i % SHADES.length], active && active !== s.id && "opacity-25")} style={{ flexGrow: s.value, flexBasis: 0 }} /> ))} </div> <ul aria-labelledby={title ? `${id}-t` : undefined} className="grid grid-cols-[minmax(0,1fr)] gap-0.5 sm:grid-cols-2 sm:gap-x-6"> {segments.map((s, i) => ( <li key={s.id} className="min-w-0"> <button type="button" aria-pressed={pinned === s.id} onClick={() => setPinned(pinned === s.id ? null : s.id)} onMouseEnter={() => setHover(s.id)} onMouseLeave={() => setHover(null)} onFocus={() => setHover(s.id)} onBlur={() => setHover(null)} className="flex min-h-9 w-full items-center gap-2.5 rounded-md px-2 text-start text-sm outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50" > <span aria-hidden="true" className={cn("size-2.5 shrink-0 rounded-[3px]", SHADES[i % SHADES.length])} /> <span className="min-w-0 flex-1 truncate">{s.label}</span> <span className="shrink-0 tabular-nums text-muted-foreground"> {s.hint && <span className="me-2 text-xs">{s.hint}</span>} <span className="font-medium text-foreground">{format(s.value)}</span> <span className="ms-1.5 text-xs">{msg("split-bar.share", "{percent}%", { percent: Math.round((s.value / sum) * 100) })}</span> </span> </button> </li> ))} </ul> </div> ) } export { SplitBar, type SplitBarProps, type SplitSegment }Update the import paths to match your project setup.
Usage
import { SplitBar } from "@/components/ballmac/split-bar"The full example is in the Code tab above.
Examples
Two shares
"use client"
import { SplitBar } from "@/components/ballmac/split-bar"
export default function SplitBarTwo() {
return (
<div className="w-full max-w-md">
<SplitBar title="Traffic" segments={[{ id: "new", label: "New visitors", value: 640 }, { id: "ret", label: "Returning", value: 360 }]} />
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
segments* | SplitSegment[] | — |
title | string | — |
formatFormats each value. | (value: number) => string | (v) => nf.format(v) |
totalShows a total above the bar. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Legend | The accessible version: buttons with label, value and percent; pressed state |
| Bar | Decoration only |
| Color | Never the only way to tell parts apart |
Use with AI
segments is [{ id, label, value, hint? }]; title, total (text above the bar), format(value). The legend lists every segment with its value and percent; hover or focus lights a slice, and a click keeps it lit (aria-pressed). With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Split Bar (@ballmac/split-bar) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Breaking a total into a handful of parts
- Spend, storage, traffic or time splits
Not for
- Many parts or nested parts; use treemap
- Ranking; use bar-list
Registry JSON: https://ui.ballmac.com/r/split-bar.json
Credits
Free to use in personal and commercial projects.
- npm
- None
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Bar List
A ranked list of bars with the name and number on top of the bar, so both stay readable at any width. Rows can be links or buttons, a long list folds to its top few, and every value is real text.
Treemap
A dependency-free treemap: each item is a rectangle sized by its share of the whole in a squarified layout, with real text on the cells, group colours and a legend with totals, and arrow keys that move to the nearest cell.
Stat Card
A metric card with a clear comparison trend, optional visual, and readable value hierarchy.
Waterfall Chart
A waterfall or bridge chart from a start to an end: floating bars for increases and decreases with a + or − sign as well as colour, totals on the baseline, connectors, real HTML labels and a table of the numbers for screen readers.
Ballmac UI Pro
Need the whole screen, not one piece?
150 premium blocks (dashboards, settings, calendars, mail, shops, onboarding), a private shadcn registry and Figma tokens. Blocks that show data come with loading, empty and error states built in.
Not ready? Try three Pro blocks free
Leave your email and the blocks appear right here, ready to copy. Yours to use in any project.