Business inputs that format while you type without moving the caret: money, numbers and percentages with locale separators, phone numbers with a country picker, grouped IBANs with bank lookup, and national / tax IDs with checksum rules. Values are always locale-free — a decimal string, E.164, a compact IBAN.
Including 20% VAT.
Installation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { CurrencyInput, Field, IbanInput, NationalIdInput, PhoneInput } from "@vamos-ui/react";
<Field label="Amount"><CurrencyInput currency="TRY" onValueChange={(v) => setAmount(v)} /></Field> // "1250.50"
<Field label="Mobile"><PhoneInput defaultCountry="TR" /></Field> // "+905321234567"
<Field label="IBAN" error={error}><IbanInput onValidate={(r) => setError(r.message)} /></Field> // "TR33…"
<Field label="ID number"><NationalIdInput /></Field> // TCKN / YKNScreen readers read the formatted value; the caret never jumps when separators appear or disappear, and Backspace on a separator deletes the digit before it.
Numbers and IBANs stay left-to-right (dir="ltr") on RTL pages.
Turkish rules come from @vamos-ui/react/locale-data (tr pack). Other countries plug in through registerIdRule and IbanInput rules.
Examples
Currency
Grouping follows the locale; either . or , starts the decimals (group separators are inserted for you). Paste "₺1.250,50" or "(42.00)" and it parses. The value is a decimal string padded to the currency's precision.
en-US · USD
value: "48500.00"tr-TR · TRY — type . or , for kuruş
value: "1250000.50"Code instead of symbol, euro after the amount in de-DE.
Number and percent
NumberInput is an ARIA spinbutton: arrow keys step, Shift steps ×10, stepper buttons are optional. PercentInput puts % where the locale does (%12,5 in Turkish).
1–250, arrow keys step, Shift ×10.
Phone
Formats for the selected country and accepts 0532…, 532…, 90532…, 0090…; typing + and a calling code switches the country. The metadata loads lazily on first mount.
Try 0532 123 45 67, 90532…, or +44 20 7183 8750.
E.164: nullIBAN
Groups of four as you type, capped at the country length, checked with mod-97 and the country's BBAN pattern. Turkish IBANs show the bank.
Turkish IBANs only. The bank appears once the bank code is typed.
National and tax IDs
Checksum rules come from locale packs (tr: TCKN, YKN, VKN). masked shows 382******50 until the field is focused.
Try 38291746550 or 99238174676.
VKN, or a sole proprietor's TCKN.
Masked until focused (KVKK).
Pattern masks
FormattedInput takes a pattern (# digit, A letter, * either) or your own formatter object.
# digit, A letter.
States
Read-only, disabled, an error from the Field, and the small size.
Invalid IBAN checksum. Check it for typos.
In a form
Each input submits its raw value under the Field's name — no parsing on the server.
API
CurrencyInput
| Prop | Type | Default |
|---|---|---|
value / defaultValueDecimal string ("1250.50"). Never a float. | string | null | — |
currencyISO 4217 code. | string | provider currency |
precisionFraction digits (TRY 2, JPY 0, KWD 3). | number | from currency |
min / maxInclusive bounds as decimal strings. Checked on blur, never clamped. | string | — |
allowNegativeCredit notes and adjustments. - toggles the sign. | boolean | false |
currencyDisplayHow the currency mark is shown; its side comes from the locale. | "narrowSymbol" | "symbol" | "code" | "none" | "narrowSymbol" |
symbolCustom mark, e.g. "TL" on printed Turkish invoices. | ReactNode | — |
alignend lines amounts up in totals and tables. | "start" | "end" | "start" |
onValueChangeEvery change, with the locale-free value and what the field shows. | (value, meta: { valid, formatted, reason?, kind? }) => void | — |
validateOnWhen the invalid state and onValidate kick in. Once an error shows it re-checks on every change, so it clears as the user fixes it. | "blur" | "change" | "blur" |
onValidateLocalized message at validation moments — pass it to <Field error>. | (result: { valid, reason?, message? }) => void | — |
nameSubmits the raw value through a hidden input (the visible input keeps the formatted text). Falls back to the Field's name. | string | — |
monoMonospace tabular digits (default on for IBAN and ID inputs). | boolean | — |
size · variant · leading · trailing · disabled · readOnly · invalidSame box as Input: density-aware sizes, ghost/nav variants, adornments, Field wiring. | Input props | — |
NumberInput · PercentInput
| Prop | Type | Default |
|---|---|---|
value / defaultValueA JS number (PercentInput: percent units, 12.5 = 12.5%). | number | null | — |
min / max / stepBounds (checked on blur) and the arrow / stepper increment. | number | step 1 |
precisionMax fraction digits; 0 = whole numbers. PercentInput default 2. | number | — |
stepperLabelled − / + buttons at the end. | boolean | false |
groupingLocale grouping separators. | boolean | true |
PhoneInput
| Prop | Type | Default |
|---|---|---|
value / defaultValueE.164 ("+905321234567"); null while the number is incomplete. | string | null | — |
country / defaultCountry / onCountryChangeSelected country (ISO 3166-1 alpha-2). | string | TR for tr locales, else the locale region |
countriesLimit the picker. | string[] | — |
preferredCountriesListed first under "Suggested". | string[] | — |
displayHow a complete number is shown after blur. | "national" | "international" | "national" |
hideCountrySingle-country forms. | boolean | false |
preloadPhoneInput()Load the metadata early (e.g. on hover of "Add contact"). | () => Promise | — |
IbanInput
| Prop | Type | Default |
|---|---|---|
value / defaultValueElectronic format, uppercase, no spaces. | string | null | — |
countriesAllowed countries; others fail with reason "country". | string[] | — |
rulesPer-country length, BBAN pattern and bank lookup from locale packs. | IbanCountryRule[] | [trIbanRule] |
showBankBank name next to the number. | boolean | (iban) => string | undefined | true |
IdInput · NationalIdInput · TaxIdInput
| Prop | Type | Default |
|---|---|---|
kindA registered rule or a rule object. meta.kind reports sub-kinds (tr-ykn). | "tr-tckn" | "tr-vkn" | "tr-tax-id" | string | IdInputRule | tr-tckn / tr-tax-id |
maskedPrivacy mask while not focused (123******01). | boolean | false |
registerIdRule(rule)Add rules from locale packs or your own (charset: digits | alphanumeric). | (rule: IdInputRule) => void | — |
FormattedInput
| Prop | Type | Default |
|---|---|---|
mask# digit, A letter, * letter or digit; other characters are literals inserted for you. | string | — |
formatterYour own rules on the same caret-preserving engine (useFormattedInput). | { accept, format, parse, toDisplay?, formatOnBlur?, validate? } | — |
uppercaseUppercase letters as typed. | boolean | — |
valueFormatRaw value with or without literals. | "unmasked" | "masked" | "unmasked" |