Currency Input
A money field whose value is an integer number of minor units, formatted only while unfocused. Reads its decimal count from Intl, so zero-decimal currencies work without a special case.
src/components/ui/currency-input.tsxAbout
From the component's own source, where it was written.
A money field whose value is an integer number of minor units.
**The value is cents, not 12.34.** Binary floating point cannot represent most decimal fractions, so 0.1 + 0.2 !== 0.3 and a cart total assembled from float prices drifts by a cent — which is a support ticket, and in some jurisdictions an accounting problem. Every payment API worth using (Stripe, Adyen, PayPal) takes integer minor units for exactly this reason, and this component hands you the value in the shape they want.
**Zero-decimal currencies are not an edge case.** JPY, KRW and VND have no minor unit: ¥1000 is 1000, not 100000. The number of decimals is read from Intl.NumberFormat for the given currency rather than assumed to be two, so those work without a special case at the call site.
**The display is formatted only when the field is not focused.** Reformatting under the caret while typing is the classic money-input bug — you type 1, it becomes $1.00, and the caret jumps behind the decimals. While focused you edit a plain string; on blur it is parsed and grouped.
**The symbol is an adornment, and appears exactly once.** It is drawn beside the field and left out of the formatted value, so it cannot render twice and cannot vanish when the field takes focus. Which side it sits on comes from the locale — en-US writes $1,299, fr-FR writes 1 299 €.
Install
npx astralyx-ui add currency-input
Usage
import { CurrencyInput } from '@/components/ui/currency-input'
<CurrencyInput value={cents} onChange={setCents} currency="EUR" />Composer
Change the props and the source below follows.
minor units: 129900
<CurrencyInput
value={amount}
onChange={setAmount}
currency="USD"
/>Examples
Cents in, cents out
<CurrencyInput value={cents} onChange={setCents} />API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
CurrencyInput
Extends Omit< ComponentProps<'input'>, 'value' | 'defaultValue' | 'onChange' | 'size' | 'type' >.
| Prop | Type | Default | Description |
|---|---|---|---|
| max | number | string | — | Clamped on blur. Also in minor units. |
| min | number | string | — | Clamped on blur. Also in minor units. |
| value | number | null | — | 1234 is $12.34. Floats cannot represent most decimal fractions, so a total assembled from float prices drifts by a cent — and every payment API takes integer minor units for that reason. |
| defaultValue | number | null | null | — |
| onChange | (minorUnits: number | null) => void | — | — |
| currency | string | 'USD' | ISO 4217. Decides the symbol, the decimals and the placement. |
| locale | string | 'en-US' | — |
| size | 'sm' | 'md' | 'lg' | 'md' | — |
| showCode | boolean | false | Show the code (USD) after the field as well as the symbol. |
| amountLabel | string | 'Amount' | Accessible name for the amount field. |
| invalid | boolean | — | — |
| type | text, not number | — | A number input lets the scroll wheel change the amount, rejects grouping separators, and its spinners are meaningless for money. |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| zero-decimal currencies | from Intl | JPY, KRW and VND have no minor unit. The decimal count is resolved per currency rather than assumed to be two. |
| formatting | on blur only | Reformatting under the caret is the classic money-input bug: you type 1, it becomes $1.00, and the caret jumps behind the decimals. |