Usage Meter
betaOne number for a whole, split into parts with a legend, like 412 GB of 1 TB or $84K of a $100K goal. Its digits roll and its parts slide when values change. Against a limit it warns, and against a goal it turns success.
Preview
limit
Storage
- Photos: 212 GB
- Videos: 128 GB
- Documents: 46 GB
- Trash: 64 GB
goal
Spring fundraiser
- Online: $48.2K
- Events: $21.5K
- Sponsors: $14K
$16.3K to go
one part
Seats
7 seats left
no max
Visits this week
- Search: 6.4K visits
- Direct: 3.1K visits
- Social: 1.9K visits
- Email: 1K visits
warnings
Q3 budget
- Ads: $26.4K
- Events: $12.1K
- Tools: $4.3K
API requests
- Search: 7.2K requests
- Uploads: 3.4K requests
tone
Storage (neutral)
588 GB left
Storage (brand)
588 GB left
Storage (danger)
588 GB left
Storage (success)
588 GB left
Storage (warning)
588 GB left
size
Storage, sm
- Photos: 212 GB
- Videos: 128 GB
- Documents: 46 GB
- Other: 26 GB
588 GB left
Storage, md
- Photos: 212 GB
- Videos: 128 GB
- Documents: 46 GB
- Other: 26 GB
588 GB left
states
Loading
Nothing yet
- Photos: 0 GB
1K GB left
Warns earlier
- Ads: $26.4K
- Events: $12.1K
- Tools: $4.3K
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/usage-meter
Use it for
- Show how much of a limit is used, like storage, a budget, seats, or an API quota, and what fills it.
- Show progress to a goal, like sales or money raised, split by where it came from. Pass goal.
- Show how a total splits with no limit, like visits by source. Leave max out.
- Pass parts in the order the bar shows them. Keep to five. Each takes the next chart color. Group small ones into Other.
- Pass unit for a word after each number, like "GB" or "seats". Pass valueFormat for money or percents, like { style: "currency", currency: "USD" }.
- A limit turns warning at 80% of max and danger once full. Pass warnAt to warn earlier, like 0.6. A goal never warns and turns success once reached.
- Pass the parts again with new values, and the digits roll and the bar slides.
- Give a part its own action, like { label: "Clean up" } on Photos. Pass action for the card's next step, like an Upgrade button.
- Pass legend={false} and size="sm" for a small space.
Avoid it for
- A task that runs, like an upload. Use Progress.
- One plain share with no parts and no card, like 72% done. Use Progress or Ring Progress.
- Many items compared side by side, or a ranking. Use Breakdown List.
- A total over time. Use a chart, like Area Chart.
- Tokens in a model's context. Use Context Window.
Props
- parts
Required. What makes up the total, in the order the bar shows them. Five at most.
{ id?: string; label: string; value: number; action?: UsageMeterPartAction }[]
- UsageMeterPartAction
Type. A quiet button at the end of the part's legend row. Pass functions from a client component.
{ label: string; onClick?: () => void; loading?: boolean; disabled?: boolean }
- max
The limit or the goal, like 1000. Left out, the bar splits the total, with no percent and no warning.
number
- goal
max is a goal to reach, not a limit. It never warns, says how much is to go, and turns success once reached.
boolean · default false
- title
The card's title. It names the meter.
ReactNode · default "Usage"
- badge
A small tag beside the title, like the plan's name.
ReactNode
- unit
A word after each number, like "GB" or "seats". Leave it out when valueFormat prints the unit.
string
- valueFormat
Formats every number. Merged over the default, so { style: "currency", currency: "USD" } prints $84K. Pass { notation: "standard" } for full numbers.
Intl.NumberFormatOptions · default { notation: "compact", maximumFractionDigits: 1 }
- warnAt
For a limit, the share of max, from 0 to 1, where it turns warning. It turns danger once full.
number · default 0.8
- tone
Colors the percent and the last line. Left out, it follows the share of max. Passed, it stays that tone.
"neutral" | "brand" | "danger" | "success" | "warning" · default "neutral"
- legend
Shows each part's name, value, and action under the bar.
boolean · default true
- action
A button in the last row, like Upgrade or Raise limit.
ReactNode
- loading
Shows placeholders while the numbers load.
boolean · default false
- size
Bar height and the number's size.
"sm" | "md" · default "md"
- locale
The locale for valueFormat.
string · default "en-US"
- className
Classes for the card. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| parts | { id?: string; label: string; value: number; action?: UsageMeterPartAction }[] | - | Required. What makes up the total, in the order the bar shows them. Five at most. |
| UsageMeterPartAction | { label: string; onClick?: () => void; loading?: boolean; disabled?: boolean } | - | Type. A quiet button at the end of the part's legend row. Pass functions from a client component. |
| max | number | - | The limit or the goal, like 1000. Left out, the bar splits the total, with no percent and no warning. |
| goal | boolean | false | max is a goal to reach, not a limit. It never warns, says how much is to go, and turns success once reached. |
| title | ReactNode | "Usage" | The card's title. It names the meter. |
| badge | ReactNode | - | A small tag beside the title, like the plan's name. |
| unit | string | - | A word after each number, like "GB" or "seats". Leave it out when valueFormat prints the unit. |
| valueFormat | Intl.NumberFormatOptions | { notation: "compact", maximumFractionDigits: 1 } | Formats every number. Merged over the default, so { style: "currency", currency: "USD" } prints $84K. Pass { notation: "standard" } for full numbers. |
| warnAt | number | 0.8 | For a limit, the share of max, from 0 to 1, where it turns warning. It turns danger once full. |
| tone | "neutral" | "brand" | "danger" | "success" | "warning" | "neutral" | Colors the percent and the last line. Left out, it follows the share of max. Passed, it stays that tone. |
| legend | boolean | true | Shows each part's name, value, and action under the bar. |
| action | ReactNode | - | A button in the last row, like Upgrade or Raise limit. |
| loading | boolean | false | Shows placeholders while the numbers load. |
| size | "sm" | "md" | "md" | Bar height and the number's size. |
| locale | string | "en-US" | The locale for valueFormat. |
| className | string | - | Classes for the card. Later classes win. |
Accessibility
- The card is a group named by the title. With max, the bar is a meter with the same name, and reads like "412 GB of 1,000 GB, 41%".
- With no max, the bar is hidden from screen readers, and the legend says each part in words.
- The bar's colored parts are hidden from screen readers. The legend says each part's name and value in text.
- The big number's rolling digits are hidden. Screen readers hear the final value only.
- A part's action is named with the part, like "Clean up Photos", so each button says what it acts on.
- The last line says Almost full, Full, or Goal reached in words, so the state does not rest on color alone.
- The bar slides and the digits roll on the motion variables, so reduced motion jumps straight to the new values.
- In forced-colors mode the bar draws a border and its parts fill with the text color.
Examples
Storage
<UsageMeter
title="Storage"
badge="Pro plan"
max={1000}
unit="GB"
valueFormat={{ notation: "standard" }}
parts={[
{ label: "Photos", value: 212 },
{ label: "Videos", value: 128 },
{ label: "Documents", value: 46 },
{ label: "Other", value: 26 },
]}
/>A budget near its limit
import { Button } from "@/components/ui/button";
import { UsageMeter } from "@/components/ui/usage-meter";
export function MarketingBudget() {
return (
<UsageMeter
title="Q3 budget"
max={50000}
valueFormat={{ style: "currency", currency: "USD" }}
parts={[
{ label: "Ads", value: 26400 },
{ label: "Events", value: 12100 },
{ label: "Tools", value: 4300 },
]}
action={
<Button size="sm" emphasis="outline">
Raise limit
</Button>
}
/>
);
}A goal
<UsageMeter
title="Spring fundraiser"
goal
max={100000}
valueFormat={{ style: "currency", currency: "USD" }}
parts={[
{ label: "Online", value: 48200 },
{ label: "Events", value: 21500 },
{ label: "Sponsors", value: 14000 },
]}
/>A total with no limit
<UsageMeter
title="Visits this week"
unit="visits"
parts={[
{ label: "Search", value: 6400 },
{ label: "Direct", value: 3100 },
{ label: "Social", value: 1900 },
{ label: "Email", value: 1000 },
]}
/>