Credit top-up
Pick an amount of credits, see the new balance and pay, for usage-based plans.
Loading preview…
Install
- 1
Run this command in your Tailwind CSS project. It adds
components/ai/credit-top-up.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/credit-top-up.json
Usage & API
Example
import { CreditTopUp } from "@/components/ai/credit-top-up"
<CreditTopUp
balance={1}
onPay={() => {}}
/>Props CreditTopUp
| Prop | Type & description | Required |
|---|---|---|
balance | number | Required |
amounts | number[] | Optional |
card | string | Optional |
state | PayState | Optional |
onPay | (amount: number) => void | Required |
onReset | () => void | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
The four default chips are radio buttons in one row. The selected chip draws a shared sand highlight and sets aria-checked. The custom control is a label around an input with inputMode numeric, the accessible name "Custom amount", and the placeholder "Other amount". The new-balance figure animates when the amount key changes. Under a divider, the card name and Change sit on one row. Change is a real button, focusable, and it has no click handler in the file. The pay button is full width. It disables when state is paying, and also when state is idle and the amount is outside 5 to 1,000. In the done state it stays enabled so onReset can run. The footer uses the range warning only when custom is selected, the field already has text, and the value is invalid: "Enter between $5 and $1,000". An empty custom field, or any preset, keeps "Credits don't expire. Used after your plan's limit." Typing 1001 is possible because four digits are allowed, and that is the case the warning is for. Typing 4 is the other. The demo's onPay sets paying immediately and, 1.4 seconds later, adds the amount to balance and sets done. Pressing "Credits added" returns to idle through onReset and does not subtract the balance, so a second purchase stacks on the new figure. The $25 chip is still selected unless the person had moved to custom.
Choosing a layout
Use it for a usage-based plan where extra credits are bought in cash, not as a silent seat change. The idle footnote is the link to Usage meter: the ring or the weekly bars are the included allowance, and these credits apply once that allowance is gone. The reached quota card says "Upgrade to Max" in fixed copy and does not collect a number. Send someone here only when the recovery is actually a credit purchase. If the higher tier is a different plan with a different message cap, keep that on the quota card's upgrade handler and do not pretend the two screens are the same offer.
Do not leave it pinned beside every message. Paying is a state you drive. The component will not charge a card, and it will not leave idle unless the state prop changes. onPay receives the numeric amount, not a currency string. You set "paying", then "done" when the charge succeeds, or back to "idle" if you want the form usable again. There is no error prop and no failure layout. A declined charge has to be told by the screen around this card. The custom field also refuses a decimal, because non-digits are stripped on input, so 25 is possible and 25.50 is not. Preset chips are whole dollars for the same reason.
Why does the new balance stop including the amount once payment finishes?
The row adds the selected amount only while state is not "done". The demo's timeout updates balance by that amount and sets state to done together. If the row still added the amount after that, $4.20 plus $25 would paint as $54.20 instead of $29.20. Showing balance alone in the done state is what prevents the double add. If you pass done without first folding the amount into balance, the row shows the old balance and looks like the payment vanished. onReset is what the done button calls. The demo's handler only returns state to idle.
What is selected before anyone clicks a chip?
The second entry of amounts on the first render. With the default list that is 25, not 10, and not a remembered last purchase. Selection is useState, not a controlled prop, so the parent cannot move the chip later without editing the file. The custom path starts unselected. Focusing the field switches to it, and the pay button stays disabled until the digits fall between 5 and 1,000. Clearing the field back to empty leaves custom selected, amount 0, the button disabled, and the ordinary footnote in place, because the range sentence requires the field to be non-empty.