Skip to content

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

All triggers

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.kmd money 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"; never numeric (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,00 in 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.5 and 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.005 and 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 inputmode attribute equals decimal

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.34 and 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 150 and 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,56 entered under a pt-BR locale and 1,234.56 entered under an en-US locale, both currency USD
  • WHEN each value is persisted
  • THEN both store the identical minor-unit integer 123456 with code USD

References