CurrencyInput
Locale-aware monetary input that stores values in minor units (cents). Raw editing on focus, formatted display with currency symbol on blur.
Playground
<CurrencyInput
label="Price"
locale="de-DE"
/>01 Examples
Default — follows the active i18n locale
<script>
let priceCents = $state(1234_56); // grouped per the active locale
</script>
<CurrencyInput label="Price" bind:value={priceCents} />USD with prefix symbol
<CurrencyInput
label="Amount"
bind:value={amountCents}
locale="en-US"
currency="USD"
symbolPosition="prefix"
/>JPY — zero decimal precision
<CurrencyInput
bind:value={yen}
locale="ja-JP"
currency="JPY"
precision={0}
/>02 Working with major units
CurrencyInput stores values in minor units (cents) so summing,
sorting, and persisting amounts stay free of floating-point drift. When integrating with an
API or datastore that uses major-unit floats (e.g. 1234.56 for €1.234,56), use
the exported centsToMajor / majorToCents helpers at the boundary — and
keep the in-memory representation in cents.
Cents in, major units out
Stored as cents: 123456 · Exported as major: 1234.56
<script>
import { CurrencyInput, centsToMajor, majorToCents } from '@urbicon-ui/blocks';
// Major-unit float arrives from an external API.
const apiAmount = 1234.56;
let cents = $state(majorToCents(apiAmount)); // 123456
const exportedAsMajor = $derived(centsToMajor(cents)); // 1234.56
</script>
<CurrencyInput label="Price" bind:value={cents} />
<p>Major: {exportedAsMajor}</p>Float-precision caveat: a major-unit value that has already lost precision
before reaching majorToCents (e.g. 0.1 + 0.2) cannot be recovered.
For values that must round-trip exactly across system boundaries, transport them as minor-unit
integers (or as strings) instead of major-unit floats.
03 Customization
CurrencyInput wraps <Input>, so all InputProps (label, helper, error, slotClasses, …) flow through. The cents-based value, locale, currency, symbolPosition, and precision props add the locale-aware behaviour on
top.
Use symbolPosition="none" for headless numeric editing where you want the locale formatting
(grouping / decimal separator) without the currency symbol.
04 Accessibility
Inherited from Input
Inherits aria-invalid / aria-describedby wiring from the
underlying <Input> via the label, error, and helper props.
Numeric keyboard
Sets inputmode="decimal" so mobile keyboards open the numeric pad with a decimal
separator.
Raw value while focused
Raw editing while focused — formatting happens on blur, so screen-reader users hear the unambiguous typed value.
05 API Reference
Prop | Type | Default | Description | |
|---|---|---|---|---|
autoComplete inherited | string | — | HTML autocomplete hint for browser autofill. | |
children inherited | Snippet | — | Snippet content rendered below the input for advanced layouts. | |
class inherited | string | — | Extra classes merged onto the root wrapper element. | |
clearable inherited | boolean | false | Show a clear button when the input has a value.
Press Escape or click the button to clear. Fires onClear after clearing. | |
currency | string | 'EUR' | ISO-4217 currency code. Determines the symbol when
CurrencyInputProps.symbolPosition is 'prefix' or 'suffix'. | |
disabled inherited | boolean | false | Whether the CurrencyInput is disabled and non-interactive | |
error inherited | string | — | Error message below the input. When set, overrides helper and
forces danger border styling regardless of intent. | |
helper inherited | string | — | Helper text below the input — hidden when error is present. | |
label inherited | string | — | Label text displayed above the input, auto-linked via for/id. | |
leftIcon inherited | Snippet | — | Icon snippet rendered on the left side of the input field. | |
leftIconAriaLabel inherited | string | — | Accessible label for the clickable left icon button. Required when
onLeftIconClick is set so screen-reader users hear a name for the
button (icons inside are aria-hidden). | |
locale | string | 'auto' | BCP 47 locale used for formatting (Intl.NumberFormat). Controls the
grouping separator (. vs ,) and decimal separator. Defaults to
'auto', which follows the active <I18nProvider> locale — SSR-safe
(server and client resolve the same locale, no hydration flash) and
consistent with the rest of the library's number formatting. Falls back
to the base locale (en) when no provider is mounted. Pass an explicit
BCP 47 string (e.g. 'de-DE', 'ja-JP') to override. currency is
intentionally **not** auto-detected, since it is orthogonal to locale
(a de-CH user may still bill in EUR). | |
mint | InputProps['mint'] | 'none' | Micro-interaction preset forwarded to the inner Input. Redeclared from InputProps so the inheritance is a documented contract rather than an accident of the Omit list. | |
name | string | — | Shared name for native form submission. When set, a hidden input
is rendered carrying the integer minor-unit value (matching
CurrencyInputProps.value) — never the locale-formatted display
string. The visible Input itself stays unnamed so the formatted text
is not submitted alongside.
Empty / null values submit as "" so the field still appears in
the FormData payload (consumers can disambiguate "untouched" via
server-side schema parsing). | |
onLeftIconClick inherited | () => void | — | When provided, the left icon becomes a clickable button. | |
onRightIconClick inherited | () => void | — | When provided, the right icon becomes a clickable button. | |
onValueChange | (cents: number | null) => void | — | Fires whenever the parsed value changes. Receives the new value in minor units (or null). | |
persistDebounceMs inherited | number | 300 | Debounce interval (ms) for storage writes. | |
persistKey inherited | string | — | Key for persisting the input value to storage. | |
persistNamespace inherited | string | — | Namespace (e.g. user id) to scope the persist key. | |
persistStorage inherited | localStoragesessionStorage | 'localStorage' | Storage backend for persistence. | |
persistVersion inherited | number | 1 | Version stamp included in the storage key. | |
precision | number | 2 | Number of fractional digits stored in CurrencyInputProps.value.
For most currencies 2; for JPY use 0, for BHD/KWD use 3. | |
preset inherited | string | — | Apply a named preset registered via <BlocksProvider presets={{ Input: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
readonly inherited | boolean | false | Readonly property for the CurrencyInput component | |
required inherited | boolean | false | Adds a required asterisk to the label and sets the native required attribute. | |
rightIcon inherited | Snippet | — | Icon snippet rendered on the right side of the input field. | |
rightIconAriaLabel inherited | string | — | Accessible label for the clickable right icon button. Required when
onRightIconClick is set so screen-reader users hear a name for the
button (icons inside are aria-hidden). | |
slotClasses inherited | Partial<Record<InputSlots, string>> | — | Per-slot class overrides merged with tv() styles. Slots: wrapper (root —
what class also targets) | container | base (the <input> element) |
label | message | iconContainer | iconButton | iconDecoration. | |
symbolPosition | CurrencySymbolPosition | 'suffix' | Where the currency symbol is rendered as a static adornment. The symbol
is shown in the input's left or right icon slot (always visible, including
while focused) — it is never embedded in the editable text. Use 'none'
for headless numeric editing (no symbol shown at all). | |
unstyled inherited | boolean | — | Remove all default tv() classes — only user-provided classes apply. | |
value | number | null | null | Current monetary value in **minor units** (e.g. cents).
Use null for "no value entered yet"; the input renders empty. |
06 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
CurrencySymbolPosition | type | helper | 1 | Where the currency symbol appears relative to the input field.
The symbol is rendered as a static adornment in the input's left or right
icon slot — never embedded in the editable text — so it stays visible during
editing and never doubles up with the locale's own currency formatting.
'none' suppresses the symbol entirely (e.g. for pure numeric editing). | |
CurrencyInputProps | interface | props | 0 | — | |
InputProps | interface | props | 0 | — | |
InputVariants | type | variant | 0 | — | |
MintProp | type | helper | 2 | — | |
InputSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. | |
VariantProps | type | helper | 0 | — | |
MintName | type | helper | 0 | A mint name: a built-in (autocompleted), 'none' to disable, or any
consumer-registered name. (string & {}) keeps the registry open — a
custom name still type-checks, it just isn't suggested. A typo therefore
also still compiles (it resolves like an unregistered custom name and
warns at runtime); the union buys completion and docs, not validation. | |
MintConfig | interface | helper | 0 | — | |
SlotNames | type | helper | 0 | Extracts the slot-name union from a slotted tv() config function — the
companion to VariantProps. The slot-mode overload returns
(props?) => { [K in keyof S]: SlotFn }, so keyof ReturnType<T> is exactly
the set of slot names a component declares in tv({ slots: … }).
Use it to type a component's slotClasses prop from the single source of
truth (its *.variants.ts) instead of hand-maintaining a parallel union
that silently drifts when a slot is added or renamed: |
07 Installation
Import
import { CurrencyInput } from '@urbicon-ui/blocks';