Skip to content

Phone input (country selector + i18n format)

components specs/components/phone-input.kmd

Country-aware phone input — country selector + locale-aware mask + ISO E.164 normalization on storage. Common need in signup / profile / SMS-verification flows across Koder products. Modeled after Base Web PhoneInput.

When this spec applies

Primary triggers

All triggers

Specification body

Component — Phone input

Status: v0.1.0 — Draft.

R1 — Anatomy

  • Country selector (combobox-style; see specs/components/combobox.kmd) showing flag glyph + dial-code (e.g. 🇧🇷 +55).
  • Inline divider.
  • Phone-number input with locale-aware mask.
  • Trailing validation icon (✓ on valid E.164, ✗ on invalid).

R2 — Default country

  • Derived from user's locale per specs/i18n/contract.kmd (en-US → US, pt-BR → BR, es-ES → ES).
  • Fallback: device locale via navigator.language (web) / Locale.getDefault() (Flutter / native) when product locale doesn't map to a country (e.g. en-US is fine; en-Latn is not).
  • Manual override always allowed — user picks any country in the selector.

R3 — Format (mask + storage)

  • Display: locale-aware mask via libphonenumber-equivalent (e.g. +55 (11) 91234-5678 for BR; +1 (415) 555-2671 for US).
  • Storage: ISO E.164 (+5511912345678). Always strip mask before emitting onChange value.
  • Paste handling: if pasted value parses as E.164 in a different country than currently selected, auto-switch the country selector (and announce via live region — see R5).

R4 — Validation

  • Inline: validate per the selected country's libphonenumber rules.
  • Validity states:
    • Empty: neutral (no icon)
    • Partial / invalid format: muted (no error icon — too noisy mid-typing)
    • Complete + valid: ✓ icon
    • Complete + invalid: ✗ icon + error message below
  • Error messages from specs/errors/user-facing-messages.kmd.

R5 — Keyboard navigation

KeyAction
Tab into selectorOpen dropdown / focus current selection
↑ / ↓ in selectorNavigate countries
Type letters in selectorJump to country starting with those letters
Tab out of selectorFocus phone input
Type / paste in inputMask applies live
Tab out of inputValidate + show error if invalid
Esc in selectorClose dropdown without changing selection

Live-region announce on country auto-switch (R3 paste): "Country changed to {country name} based on pasted number."

R6 — Accessibility

  • Both selector and input have associated labels.
  • Selector: role="combobox" per specs/components/combobox.kmd R3.
  • Phone input: <input type="tel" inputmode="tel"> so mobile keyboards show the dial pad.
  • Validation icon + error message linked via aria-describedby.

R7 — i18n

  • Country names translated per locale (per specs/i18n/contract.kmd).
  • Mask uses the country's local convention regardless of UI locale (a BR phone number is always masked +55 (XX) XXXXX-XXXX even when the UI is in English).
  • Dial codes are universal (no translation needed).

R8 — OUIA

Per specs/testing/ouia-test-hooks.kmd:

  • data-ouia-component-type="PhoneInput"
  • data-ouia-component-id="<input-id>"
  • data-ouia-safe="true" always (input doesn't have async ready states beyond the country list load).

Não-escopo

  • SMS verification flow (consumer concern; phone-input emits valid E.164 only, downstream handles the OTP loop).
  • libphonenumber library binding (impl detail — Google libphonenumber-js for web, libphonenumber-dart for Flutter).
  • Phone-extension support ("ext. 1234") — out of v0; add if a product needs it.

References