Skip to content
caliper ui docs

Components

Split bar

One bar that is the whole, split into shares in the chart colours. Rest on a share, or focus its legend row, and it widens to show its figures while the others make room; switch shares off and the rest fill the bar again.

Free
pnpm dlx shadcn@latest add @caliper/split-bar

Monthly recurring revenue

October, by plan

Revenue by plan$187,200
9.8%28.3%38%20.6%3.3%
Installs components/ui/split-bar.tsxDepends on none beyond ReactMotion none in component source

When to use

Use it for

  • A single breakdown that must fit in a row: revenue by plan, storage by type, time by project.
  • Dashboards where a donut takes too much height.

Skip it when

  • Comparing the same breakdown over time; use a stacked bar chart.
  • More than six or seven shares; small ones become slivers.

Decisions

Why it is built this way, so you know what to keep when you change your copy.

Behaviour

Focus widens
The focused share gains an eighth of the bar on top of its size, so even a 2% sliver opens up enough to read its percentage.
Percentages of what is left
Switching a share off recomputes every percentage over the shares still shown, and the bar always keeps one.

Usage

Legend is the keyboard path
The bar is one image with every share in its name; the legend rows are toggle buttons that also focus their share.

Installation

terminal
pnpm dlx shadcn@latest add @caliper/split-bar

Adds its 2 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage

This is the code behind the preview above.

example.tsx
import { type ReactNode } from "react"
import { SplitBar } from "@/components/ui/split-bar"

function Panel({ title, context, className = "max-w-md", children }: { title: string; context: string; className?: string; children: ReactNode }) {
  return (
    <section className={`grid w-full gap-5 rounded-overlay border border-border bg-background p-6 shadow-resting max-sm:p-4 ${className}`}>
      <div className="grid gap-1">
        <p className="font-medium">{title}</p>
        <p className="text-sm text-muted-foreground">{context}</p>
      </div>
      {children}
    </section>
  );
}

const plans = [
  { id: "starter", label: "Starter", value: 18400 },
  { id: "team", label: "Team", value: 52900 },
  { id: "business", label: "Business", value: 71200 },
  { id: "enterprise", label: "Enterprise", value: 38600 },
  { id: "add-ons", label: "Add-ons", value: 6100 },
];

export function SplitBarDemo() {
  return (
    <Panel title="Monthly recurring revenue" context="October, by plan" className="max-w-lg">
      <SplitBar label="Revenue by plan" segments={plans} format={(value) => `$${value.toLocaleString("en-US")}`} />
    </Panel>
  );
}

API reference

SplitBar

component

Also accepts the native props of its root element and a ref.

PropTypeDefaultDescription
label *string—Names the whole, e.g. "Revenue by plan".
segments *SplitBarSegment[]—
format((value: number) => string)(value) => value.toLocaleString("en-US")Formats a segment's own value for the readout.

Accessibility

It exposes img semantics, with aria-live, aria-label, aria-pressed, aria-disabled and aria-hidden kept in step with state. Changes that matter are announced through a live region.

No custom key handling; Tab and the native activation keys do the work.

Motion

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.

All Changing values examples