Breakdown List
betaRows with their number and a pill bar, inside a card, for parts of a whole or a ranking, with targets, status tones, and change words.
Preview
status
- Design: 92%, target 80%, Up 6
- Engineering: 81%, target 80%, Up 2
- Support: 64%, target 80%, Down 4
- Marketing: no figure, target 80%
currency
- Venue hire: $4,200
- T-shirts: $2,650
- Travel: $1,180
people
- Ava Clarke: 14
- Sam Okafor: 11
- Mia Hughes: 9
- Leo Martin: 6
swatches
- Starter: 38
- Team: 31
- Business: 22
loading
Loading
empty
No replies yet.
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/breakdown-list
Use it for
- Parts of a whole or a ranking with readable labels, like survey replies by team or tickets closed by person.
- Pass items, each with a label and a value, and a title or aria-label.
- Pass max for the value of a full bar, like 100 for percents. It defaults to the largest value.
- Give an item a target to draw a tick on its bar, and tone='success' or 'warning' when its value is a status.
- Give an item a change, like { text: 'Up 6', tone: 'success' }, for a short word on the right.
- Put an icon, a person's photo, or a color swatch in icon.
- Pass value: null when a row has no figure. It shows a dash, not a zero.
Avoid it for
- Steps that narrow, like sign-ups to purchases. Use FunnelChart.
- A trend over time. Use LineChart or AreaChart.
- Many categories compared at once with several series. Use BarChart.
- More rows than people can scan, like 50. Show the top ten and link to the rest.
Props
- items
Required. One row each, in order. A row's tone colors its bar as a status. Left out, the bar takes --chart-1.
{ id?: string; label: string; value: number | null; meta?: ReactNode; icon?: ReactNode; target?: number; tone?: "success" | "warning" | "danger"; change?: { text: string; tone?: "success" | "warning" | "danger" } }[]
- title
A visible title. It names the list. TypeScript requires title, aria-label, or aria-labelledby.
ReactNode
- aria-label
Names the list when it has no visible title.
string
- max
The value of a full bar. Default: the largest value.
number
- valueFormat
How values and targets print. Options work from a server component. A function works from a client component only.
Intl.NumberFormatOptions | ((value: number) => string)
- locale
The locale for valueFormat options.
string · default "en-US"
- loading
Shows placeholder rows, as many as items, or three.
boolean · default false
- emptyText
Shown when items is empty.
ReactNode · default "No data yet"
- className
Extra classes for the card. Later classes win, so 'border-0 p-0' drops the card's edge and padding.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| items | { id?: string; label: string; value: number | null; meta?: ReactNode; icon?: ReactNode; target?: number; tone?: "success" | "warning" | "danger"; change?: { text: string; tone?: "success" | "warning" | "danger" } }[] | - | Required. One row each, in order. A row's tone colors its bar as a status. Left out, the bar takes --chart-1. |
| title | ReactNode | - | A visible title. It names the list. TypeScript requires title, aria-label, or aria-labelledby. |
| aria-label | string | - | Names the list when it has no visible title. |
| max | number | - | The value of a full bar. Default: the largest value. |
| valueFormat | Intl.NumberFormatOptions | ((value: number) => string) | - | How values and targets print. Options work from a server component. A function works from a client component only. |
| locale | string | "en-US" | The locale for valueFormat options. |
| loading | boolean | false | Shows placeholder rows, as many as items, or three. |
| emptyText | ReactNode | "No data yet" | Shown when items is empty. |
| className | string | - | Extra classes for the card. Later classes win, so 'border-0 p-0' drops the card's edge and padding. |
Accessibility
- Renders a <figure> named by its title, or by aria-label. TypeScript requires one. The rows are a list.
- Each row reads as its label, then its value, its target, and its change, like 'Design: 84%, target 80%, Up 6'.
- A row with no value reads 'no figure', and shows a dash with no bar, so it never reads as zero.
- The bars are hidden from screen readers. The track is quiet, so the number in text carries the meaning.
- Status bars use the tone's color, and plain bars use --chart-1, which the theme holds to 3:1 on the page.
- Bars grow in once the list is on screen, over three times --motion-duration-base. Reduced motion and motion off draw them at once.
- Loading is read out through a status line that stays on the page.
Examples
Status bars against a target
<BreakdownList
title="Survey replies by team"
max={100}
valueFormat={{ style: "unit", unit: "percent" }}
items={[
{ label: "Design", value: 92, target: 80, tone: "success", change: { text: "Up 6", tone: "success" } },
{ label: "Engineering", value: 64, target: 80, tone: "warning" },
{ label: "Marketing", value: null },
]}
/>A ranking with icons
import { Trophy, UserRound } from "lucide-react";
<BreakdownList
aria-label="Tickets closed by person"
items={[
{ label: "Ava Clarke", value: 14, icon: <Trophy /> },
{ label: "Sam Okafor", value: 11, icon: <UserRound /> },
]}
/>While it loads
<BreakdownList title="Survey replies by team" items={data ?? []} loading={isLoading} />