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.tsx

About

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

Props
tsx
<CurrencyInput
  value={amount}
  onChange={setAmount}
  currency="USD"
/>

Examples

Cents in, cents out

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' >.

PropTypeDefaultDescription
maxnumber | string—Clamped on blur. Also in minor units.
minnumber | string—Clamped on blur. Also in minor units.
valuenumber | 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.
defaultValuenumber | nullnull—
onChange(minorUnits: number | null) => void——
currencystring'USD'ISO 4217. Decides the symbol, the decimals and the placement.
localestring'en-US'—
size'sm' | 'md' | 'lg''md'—
showCodebooleanfalseShow the code (USD) after the field as well as the symbol.
amountLabelstring'Amount'Accessible name for the amount field.
invalidboolean——
typetext, not number—A number input lets the scroll wheel change the amount, rejects grouping separators, and its spinners are meaningless for money.

Behaviour

TopicDetailDescription
zero-decimal currenciesfrom IntlJPY, KRW and VND have no minor unit. The decimal count is resolved per currency rather than assumed to be two.
formattingon blur onlyReformatting under the caret is the classic money-input bug: you type 1, it becomes $1.00, and the caret jumps behind the decimals.