Skip to content
txbrendan/ui

Usage Meter

beta

One 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

Pro plan
450of 500 GB
  • Photos: 212 GB
  • Videos: 128 GB
  • Documents: 46 GB
  • Trash: 64 GB
Almost full · 50 GB left

goal

Spring fundraiser

$83.7Kof $100K
  • Online: $48.2K
  • Events: $21.5K
  • Sponsors: $14K

$16.3K to go

one part

Seats

18of 25 seats

7 seats left

no max

Visits this week

12.4Kvisits
  • Search: 6.4K visits
  • Direct: 3.1K visits
  • Social: 1.9K visits
  • Email: 1K visits

warnings

Q3 budget

$42.8Kof $50K
  • Ads: $26.4K
  • Events: $12.1K
  • Tools: $4.3K
Almost full · $7.2K left

API requests

Free tier
10.6Kof 10K requests
  • Search: 7.2K requests
  • Uploads: 3.4K requests
Full · 600 requests over

tone

Storage (neutral)

412of 1K GB

588 GB left

Storage (brand)

412of 1K GB

588 GB left

Storage (danger)

412of 1K GB

588 GB left

Storage (success)

412of 1K GB

588 GB left

Storage (warning)

412of 1K GB

588 GB left

size

Storage, sm

412of 1K GB
  • Photos: 212 GB
  • Videos: 128 GB
  • Documents: 46 GB
  • Other: 26 GB

588 GB left

Storage, md

412of 1K GB
  • Photos: 212 GB
  • Videos: 128 GB
  • Documents: 46 GB
  • Other: 26 GB

588 GB left

states

Loading

Nothing yet

0of 1K GB
  • Photos: 0 GB

1K GB left

Warns earlier

$42.8Kof $70K
  • Ads: $26.4K
  • Events: $12.1K
  • Tools: $4.3K
Almost full · $27.2K left

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. 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

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 },
  ]}
/>