Installation
npx shadcn@latest add @pixeldosa/credits-meterRequires `cn` at `@/lib/utils`. Keep `nextAction.label` specific to the click it sits beside: '8 images · 1024 × 1024', not 'Generate'.
Usage
import { CreditsMeter } from "@/components/ui/credits-meter";
<CreditsMeter
remaining="120 credits"
allowance="2,000 this month"
used={0.94}
low
nextAction={{ label: "4 images · 1024 × 1024", cost: "40 credits", affordable: true }}
resets="Resets on 1 October"
recent="12 refunded yesterday when a batch failed"
onTopUp={openBilling}
/>
Why it exists
Credit opacity is the first frustration buyers name in reviews of these tools. The specific complaint isn't the price, it's that you can't tell what an action will cost until it's already gone — and refunds, when they happen, are invisible.
Features
- The action and its price are one object, so they can never be rendered apart.
- "More than you have" is stated when the next action exceeds the balance, before the click.
- Refunds have somewhere to appear. Pipelines refund routinely; interfaces rarely say so.
- Expiry in plain words: "Resets on 1 October".
- Measured only. The bar is drawn from a real
usedvalue, never an estimate. - The bar empties as the balance falls. You pass what's been spent; it draws what's left, so the graphic and the figure above it never say opposite things.
Variants
Enough left
Credits
1,250 credits of 2,000 this month
8 images · 1024 × 102480 credits
Resets on 1 October
Not enough for this action
Credits
20 credits of 2,000 this month
4 images · upscaled to 4K260 credits · more than you have
Resets on 1 October · 12 refunded yesterday when a batch failed
Props
| Prop | Type | Notes |
|---|---|---|
remaining | string | Required, formatted by you. |
allowance | string | The full allowance, when there is one. |
used | number | Measured, 0–1, the share already spent. The bar draws the remainder. |
nextAction | { label, cost, affordable? } | The click this sits beside, and its price. |
resets | string | When the balance renews or expires. |
recent | string | Recent movement, including refunds. |
low | boolean | Marks a balance worth flagging. |
onTopUp / topUpLabel | () => void / string | |
label | string | Defaults to "Credits". |
Accessibility
- A labelled group; every figure is text, and the bar is decorative.
- "More than you have" is a sentence, not only a colour.
When not to use
- A billing page. This is a decision aid beside an action.
- Raw token counts. Convert to the unit people are charged in.
- A whole run's estimate before it starts: use
Cost Estimate.
why it's built this way
Engineering Notes
`nextAction` is a single object rather than separate label and cost props so the two can never be rendered apart: a price with no action, or an action with no price, is the pattern the component exists to replace. `affordable` is a caller flag instead of something derived from parsing the strings, because balances and costs arrive as formatted text in whatever unit and locale the product uses, and a component that tried to compare them would have to parse money. The bar is drawn only from a measured `used` value, which keeps it consistent with the rest of the system's rule that a proportion must be real, and it draws what is *left* rather than what is spent: it sits under a figure that counts down, and a bar filling up beside a balance running out reads as plenty-left at the exact moment it is not. Refunds get a place in `recent` because generation pipelines refund routinely and almost no interface shows it, which leaves people believing failures cost them money.
how it moves, and when it doesn't
Motion Notes
Nothing animates. A balance is a fact and the next action's price is a decision point, so neither has anything to express through motion; only the standard instant colour change on the top-up button, disabled under `prefers-reduced-motion`.