Skip to content
Devix Open Source
UI component v1.0.0 MIT Alpha

Amount Input

Currency input with locale formatting for AED, PKR, SAR and more.

npm install @devix-labs/amount-input
Vanilla JS React Vue Svelte
Every currency has its own number of decimals Open

Four and a quarter million downloads a week in this space, and every popular option is locked to a framework: react-number-format takes 3.7M/wk and is React-only with 229 open issues, vue-currency-input is Vue, ngx-currency is Angular, and react-currency-input-field takes 490k/wk with exactly one open issue — its maintainer asking for help. For a Laravel or Astro page the only answer is autoNumeric at a measured 43.6 kB. This is 3.6 kB, and it never hands you a number: the state is the digits typed, getValue() gives '1234.56' and getMinor() gives '123456', which is precisely what Devix VAT Calculator takes. The number of decimal places comes from ISO 4217 rather than Intl, because the two disagree for eight currencies and Intl says the Pakistani rupee has none — a field built on it silently refuses the paisa. The caret is counted in digits so a grouping separator appearing mid-number cannot move it, both decimal separators are accepted in either locale, Arabic-Indic digits are read and written, and typing: 'cents' fills from the right the way a card terminal does.

What you get

It never gives you a float

vue-currency-input's open issue #346 is titled 'Drop MAX_SAFE_INTEGER limitation' — that is the shape of the problem. Here the state is the digits typed and every value is a string, so an amount cannot come back a fils out because it went through a double.

3.6 kB, and it needs no framework

The leaders are React-only, Vue-only or Angular-only. The one framework-free option is autoNumeric at a measured 43.6 kB gzipped, whose most-reacted open issue proposes moving the project off GitHub. This works on a plain page.

The decimals come from ISO 4217, not Intl

Intl reports CLDR's display convention, and for eight currencies that is not the minor unit. It says the Pakistani rupee has no decimal places; paisa exist. Three for the Kuwaiti, Bahraini, Omani and Jordanian dinar, none for the yen — from the standard.

The caret is counted in digits

Typing the fourth digit turns 1234 into 1,234 and moves every character after it. Counting digits rather than characters makes that a non-event, so you can type into the middle of an amount and backspace over a separator without anything jumping.

Type the cents first

typing: 'cents' and the keypad behaves like a card terminal — 1 is 0.01, 1234 is 12.34, and backspace shifts it all back out. Asked for in react-number-format #366 and react-input-mask #120, and shipped by nobody.

Everyone can type into it

Two different separators means the last one is the decimal point, which holds in every locale — so 1.234,56 and 1,234.56 are the same amount in either field. Arabic-Indic and Persian digits are read as digits and written back where the locale asks.

A hundredfold error it refuses to make

Type a decimal point into a yen field and the obvious thing is to drop it — which turns ¥1,234 into ¥123,456 and looks exactly like a typo. Instead the separator stays and the digits after it are refused, so the field visibly stops changing.

The clean value posts itself

A hidden field carries 1234.56 under your field's own name, so an ordinary form submit never sends AED 1,234.56 to your server. No JavaScript of yours, and nothing to strip on the way in.

Amount Input — overview

Why it exists

Four and a quarter million downloads a week, and every popular option is locked to a framework.

react-number-format takes 3.7 million and is React-only, with 229 open issues. vue-currency-input is Vue. ngx-currency is Angular. react-currency-input-field takes 490,000 a week and has exactly one open issue: #376, "Maintainer Availability & Contributions Welcome", posted by the maintainer.

If you are writing a Laravel page, an Astro page, or anything without a build step, the only answer is autoNumeric at 43.6 kB gzipped, whose most-reacted open issue (#587) proposes moving the project off GitHub entirely.

This is 3.6 kB.

No floats, anywhere

vue-currency-input #346 is titled "Drop MAX_SAFE_INTEGER limitation". That is the shape of the problem: these libraries hand you a number, and an amount that has been through a double can be wrong by a fils. Money that is wrong by a fils is the thing an accountant notices.

Here the state is the digits that were typed. getValue() returns '1234.56' and getMinor() returns '123456', both strings. getNumber() exists and the documentation says plainly where it stops being exact.

getMinor() is exactly what Devix VAT Calculator takes, so the amount typed on the page and the tax computed from it cannot drift.

The decimals come from the standard, not from Intl

new Intl.NumberFormat('en', { style: 'currency', currency: 'PKR' })
  .resolvedOptions().maximumFractionDigits;   // 0

CLDR reports a display convention, which is not always the currency's minor unit. It says the Pakistani rupee has no decimal places. ISO 4217 says two, and paisa exist. The two disagree for eight currencies — PKR, IDR, IRR, AFN, IQD, SYP, YER and SOS — and a field built on Intl silently refuses the fraction somebody just typed.

So the minor unit comes from ISO 4217. Everything else — separators, digit shapes, grouping sizes, where the currency sits — comes from Intl, which is authoritative on all of it, and knows that en-IN groups 12,34,567 and that German puts the euro after the number.

The caret

Typing the fourth digit turns 1234 into 1,234 and moves every character after it. That is why the caret is counted in digits rather than characters — the same architecture as Devix Masked Input, and it matters more here, because the separators move as the number grows.

The fraction is not padded while the field has focus, because a field that turns 12.5 into 12.50 under the caret is one you cannot type 12.55 into. It is padded when you leave.

Typing the cents first

typing: 'cents' and the keypad behaves like a card terminal: 1 is 0.01, 12 is 0.12, 1234 is 12.34, and backspace shifts it all back out. It is react-number-format #366 (+17) and react-input-mask #120 (+25) — asked for repeatedly, shipped by nobody.

Everyone can type into it

Two different separator characters means the last one is the decimal point, which is true in every locale — so 1.234,56 and 1,234.56 are the same amount whichever field they land in. That is cleave.js #405 (+13), react-number-format #365 (+17) and autoNumeric #643, all still open.

Arabic-Indic, Persian, Devanagari and Thai digits are read as digits, because a phone keyboard in those places produces them, and written back where the locale asks for them.

A hundredfold error we refuse to make

Type 1234.56 into a yen field. The yen has no sub-unit, so the obvious thing to do with the decimal point is drop it — which turns ¥1,234 into ¥123,456 and looks exactly like a typo.

Instead the separator stays on screen and every digit after it is refused. The field visibly stops changing, which is information.

What we do not claim

That react-number-format is bad. At 3.7 million downloads a week it is the default for good reason, and it does far more than amounts. The claim is that it is React-only, that it hands you a float, and that the framework-free alternative is 43.6 kB.

That we do arithmetic. Adding lines, applying tax and rounding an invoice are Devix VAT Calculator, which takes the strings this produces. Rounding has four modes and two levels, and they belong to the invoice rather than to the input.

How it compares

Questions

Why is there no number in the API?

Because an amount that has been through a double can be wrong by a fils, and an invoice that is wrong by a fils is the thing an accountant notices. getValue() gives a decimal string and getMinor() gives whole minor units. getNumber() exists for when you know the figure is small enough, and the docs say where it stops being exact.

Why not trust Intl for the number of decimals?

Because Intl reports CLDR's display convention, not the currency's minor unit, and they differ for PKR, IDR, IRR, AFN, IQD, SYP, YER and SOS. Intl says the Pakistani rupee has no decimal places. A field built on that silently refuses the paisa someone just typed. Everything else — separators, digit shapes, grouping, where the currency sits — does come from Intl, which is authoritative on all of it.

Can it add up my invoice?

No, and deliberately. Adding lines, applying tax and rounding are Devix VAT Calculator, which takes exactly the strings this produces. Rounding has four modes and two levels, and those belong to the invoice rather than to a field — a field that quietly rounds what you typed is a field you cannot trust.

What happens if someone pastes 'AED 1,234.56'?

It reads as 1234.56. The currency code is ignored, the grouping separators are ignored, and the decimal point is found — the same routine handles typing and pasting, so anything copied out of a spreadsheet or another system lands correctly.

Why not <input type=number>?

Because it refuses grouping separators, disagrees with itself across browsers about what a valid value is, comes with a spinner nobody wants on money, and gives you a float. This is a text input with inputmode=decimal, which gets the same keyboard on a phone without any of that.