Money input component
components specs/components/money-input.kmd
A currency-aware amount field — optional currency selector, per-currency decimal precision, locale-grouped formatting on blur, robust parsing of what the user types, and a minor-unit storage contract. Pairs the money display mask (data/masks.kmd) with input behavior. Modeled after Polaris (price fields). Used by Koder Exchange, Invest, Billing, marketplace, checkout.
When this spec applies
Primary triggers
- Implement a money/price input field
All triggers
- Capture a price / amount / money value
- Add a currency selector to an amount field
- Format and validate money input
Specification body
Component — Money input
Status: v0.1.0 — Draft. Promoted from the Shopify Polaris parity scan (meta/docs/stack #091). Source: https://polaris.shopify.com/components/selection-and-input/text-field (price/currency usage). Display mask:
specs/data/masks.kmd.
R1 — Currency
- The field has an associated currency. Either fixed (shown as a
prefix/suffix affix, e.g.
R$) or selectable via an adjacent currency selector when the surface supports multiple currencies. - Changing the selected currency re-applies that currency's precision (R2) and symbol; it never silently rescales the entered numeric amount.
R2 — Precision per currency
- Decimal places follow the currency, not the locale: BRL/USD/EUR = 2, JPY/CLP = 0, BHD/KWD = 3. Use the ISO-4217 minor-unit exponent, not a hardcoded 2.
- On blur, the value is rounded to that precision (half-up); the input shows the formatted amount.
R3 — Formatting & parsing
- Display (on blur / read): grouped per the UI locale
(
specs/data/masks.kmdmoney mask) with the currency affix. - Parsing (what the user types): accept the locale's decimal + grouping separators, an optional leading sign, and a pasted formatted string; reject letters/extra symbols. Be liberal in, strict out.
inputmode="decimal"; nevernumeric(decimals needed).
R4 — Storage contract
- Persist as an integer in the currency's minor units (cents) +
the currency code — never a float (no binary-float money), aligned with
specs/data/field-types.kmd. Display is derived from this pair.
R5 — Range & validation
- Optional min/max + non-negative constraints; violations show an inline
message (
specs/errors/user-facing-messages.kmd), never silent clamp. - Empty vs zero are distinct (empty = unset).
R6 — Accessibility & i18n
- Real
<label>; the currency affix/selector is part of the accessible name ("Amount in Brazilian Real"). - Right-alignment is presentational only; the value reads correctly to AT.
- All formatting/parsing is locale + currency aware
(
specs/i18n/contract.kmd); the stored minor-unit value is locale-invariant.
Não-escopo
- FX conversion between currencies (a service concern).
- The money display mask mechanics (
specs/data/masks.kmd). - Accounting/rounding policy beyond per-currency precision (domain rules).
Requirements (testable)
Requirement: Currency association and switching {#req-money-input-currency}
The field SHALL have an associated currency — either a fixed currency shown as a prefix/suffix affix, or one chosen via an adjacent currency selector when the surface supports multiple currencies. Changing the selected currency SHALL re-apply that currency's precision and symbol and SHALL NOT silently rescale the entered numeric amount.
Scenario: Fixed currency renders as an affix
- GIVEN a money input configured with a fixed currency BRL
- WHEN the field renders
- THEN the
R$affix is shown adjacent to the amount - AND no currency selector control is present
Scenario: Switching currency re-applies precision and symbol without rescaling
- GIVEN a multi-currency money input showing
1.234,00in BRL (2 decimals) - WHEN the user changes the selected currency to JPY (0 decimals)
- THEN the affix updates to the JPY symbol
- AND the amount is re-formatted to JPY precision (0 decimals)
- BUT the underlying numeric amount is not multiplied or divided by any factor
Requirement: Per-currency decimal precision {#req-money-input-precision}
Decimal places SHALL follow the currency's ISO-4217 minor-unit exponent (BRL/USD/EUR = 2, JPY/CLP = 0, BHD/KWD = 3), not a hardcoded 2 and not the locale. On blur the value SHALL be rounded half-up to that precision and displayed formatted.
Scenario: Three-decimal currency keeps three fraction digits
- GIVEN a money input with currency BHD (minor-unit exponent 3)
- WHEN the user enters
1.5and blurs the field - THEN the displayed value shows three fraction digits (
1.500)
Scenario: Blur rounds half-up to currency precision
- GIVEN a money input with currency USD (2 decimals)
- WHEN the user enters
2.005and blurs the field - THEN the value is rounded half-up to
2.01 - AND the input shows the formatted amount
Requirement: Liberal parsing and grouped display {#req-money-input-format-parse}
The field SHALL be liberal on input and strict on output: parsing SHALL accept the locale's decimal and grouping separators, an optional leading sign, and a pasted formatted string, and SHALL reject letters and extraneous symbols; on blur/read the value SHALL be displayed grouped per the UI locale with the currency affix. The field SHALL set inputmode="decimal" and SHALL NOT use inputmode="numeric".
Scenario: Pasted formatted string is parsed
- GIVEN a money input using a pt-BR locale (
.grouping,,decimal) - WHEN the user pastes
1.234,56 - THEN the field parses it to the numeric amount 1234.56
- AND on blur displays it grouped with the currency affix
Scenario: Letters are rejected
- GIVEN a money input
- WHEN the user types
12ab3 - THEN the non-numeric characters are rejected and not accepted into the value
Scenario: Input mode is decimal
- GIVEN a money input
- WHEN the field renders
- THEN its
inputmodeattribute equalsdecimal
Requirement: Minor-unit integer storage {#req-money-input-storage}
The value SHALL be persisted as an integer in the currency's minor units (e.g. cents) paired with the currency code, and SHALL NOT be stored as a binary float. The displayed amount SHALL be derived from that integer-plus-code pair.
Scenario: Entered amount persists as minor-unit integer plus code
- GIVEN a money input with currency USD
- WHEN the user enters
12.34and the value is committed - THEN the persisted amount is the integer
1234 - AND it is paired with the currency code
USD - AND the stored amount is not a floating-point number
Requirement: Range validation and empty-vs-zero distinction {#req-money-input-validation}
When min/max or non-negative constraints are configured, a violation SHALL surface an inline validation message and SHALL NOT silently clamp the value. An empty field and a zero value SHALL remain distinct, with empty meaning unset.
Scenario: Out-of-range value shows an inline message rather than clamping
- GIVEN a money input with a configured maximum of 100
- WHEN the user enters
150and commits - THEN an inline validation message is shown
- AND the value is not silently clamped to 100
Scenario: Empty is distinct from zero
- GIVEN a money input left blank
- WHEN its value is read
- THEN it reports as unset (empty), not as the numeric value 0
Requirement: Accessible name and locale-invariant reading {#req-money-input-a11y}
The field SHALL have a real <label>, and the currency affix or selector SHALL be included in the accessible name (e.g. "Amount in Brazilian Real"). Right-alignment SHALL be presentational only so the value reads correctly to assistive technology, and while formatting and parsing are locale plus currency aware, the stored minor-unit value SHALL be locale-invariant.
Scenario: Currency is part of the accessible name
- GIVEN a money input labelled "Amount" with a fixed BRL currency
- WHEN assistive technology computes the field's accessible name
- THEN the name includes the currency (e.g. "Amount in Brazilian Real")
Scenario: Same amount stores identically across locales
- GIVEN the amount
1.234,56entered under a pt-BR locale and1,234.56entered under an en-US locale, both currency USD - WHEN each value is persisted
- THEN both store the identical minor-unit integer
123456with codeUSD
References
specs/data/masks.kmdspecs/data/field-types.kmdspecs/errors/user-facing-messages.kmdspecs/i18n/contract.kmd