An amount field that stores integer minor units and adapts its precision to the selected currency, instead of assuming two decimal places.
Written by
Ahmed Amr
Year
2026
Status
Prototype
Built with
React, TypeScript, Intl.NumberFormat
How much of a money input's correctness can be pushed into the interface layer, before the API is involved at all?
In this article4
Loading lab note
Multi-currency amount input
US Dollar uses 2 decimals. Formatting is applied on blur so it never fights what you are typing.
Stored (minor units)
125075
Displayed
$1,250.75
What I was testing
Two failure modes that are cheap to prevent in the input and expensive to fix
downstream.
Floating point.0.1 + 0.2 is 0.30000000000000004. Money held as a
JavaScript number with a decimal point is money that will eventually be wrong
by a cent, in a system where being wrong by a cent is the whole problem. The
input therefore never holds a decimal value: it holds an integer count of minor
units, and the decimal point exists only in the rendering.
Assumed precision. Two decimal places is a Western-currency habit, not a
rule. JPY has no minor unit; KWD, BHD and OMR have three. An input hard-coded to
two either refuses valid amounts or silently truncates them.
Digits to minor unitscurrency-amount-input.tsx
/** Digits typed by the user → integer minor units. No floating point involved. */function toMinorUnits(input: string, decimals: number): number | null { const cleaned = input.replace(/[^\d.]/g, ""); if (cleaned === "" || cleaned === ".") return null; const [whole = "0", fraction = ""] = cleaned.split("."); const padded = fraction.slice(0, decimals).padEnd(decimals, "0"); const combined = `${whole}${padded}`.replace(/^0+(?=\d)/, ""); const parsed = Number.parseInt(combined || "0", 10); return Number.isSafeInteger(parsed) ? parsed : null;}
Interaction decisions
Format on blur, not on keystroke
Chose
The field shows raw digits while focused and a formatted amount once focus leaves.
Trade-off
The value visibly changes when you tab away, which is a small surprise the first time. Formatting during typing is worse: inserting separators moves the caret, and every implementation of caret restoration fights the user at some point, usually while they are correcting a typo in the middle of a number.
Switching currency re-quantises the amount to the new currency's precision rather
than keeping digits that currency cannot represent. Entering 1250.75 and
switching to JPY gives 1251, not a value with a hidden fractional part waiting
to surprise someone at settlement.
Accessibility notes
inputMode="decimal" gives a numeric keypad on mobile while keeping a text
input, so formatting characters remain allowed.
The precision rule is stated in text and wired up with aria-describedby,
rather than being discoverable only by trying it.
The formatted result is in a polite live region, so the outcome of blurring is
announced.
Tabular figures, so the amount does not shift horizontally as digits change.
Still open
Negative amounts are not handled. In most business-finance contexts, direction
belongs to the transaction type, not to the sign of the amount, but that is a
product decision rather than a settled one. Paste handling also needs work:
pasting $1,250.75 currently works by accident, not by design.