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.

Requirements (testable)

Requirement: Default country from locale {#req-phone-input-default-country}

The phone input SHALL derive its initial country from the product locale per specs/i18n/contract.kmd, SHALL fall back to the device locale when the product locale does not map to a country, and SHALL always allow the user to override the selection manually.

Scenario: Product locale maps to a country

  • GIVEN the product locale is pt-BR
  • WHEN the phone input mounts
  • THEN the country selector shows BR (🇧🇷 +55) as the selected country

Scenario: Product locale does not map, device locale used

  • GIVEN the product locale is en-Latn which does not map to a country
  • AND the device locale (navigator.language / Locale.getDefault()) is en-US
  • WHEN the phone input mounts
  • THEN the country selector shows US (+1) as the selected country

Scenario: Manual override

  • GIVEN the country selector shows BR
  • WHEN the user opens the selector and picks ES
  • THEN the selected country becomes ES (+34)
  • AND the input mask switches to the ES convention

Requirement: E.164 storage normalization {#req-phone-input-e164-storage}

The phone input SHALL display the number using the selected country's locale-aware mask, and SHALL emit the onChange value as ISO E.164 with the mask stripped.

Scenario: Masked display, E.164 emitted

  • GIVEN the selected country is BR
  • WHEN the user types 11912345678
  • THEN the visible field reads +55 (11) 91234-5678
  • AND the emitted onChange value is +5511912345678

Scenario: Paste auto-switches country

  • GIVEN the selected country is BR
  • WHEN the user pastes +14155552671
  • THEN the country selector auto-switches to US
  • AND a live region announces "Country changed to United States based on pasted number."

Requirement: Validation state feedback {#req-phone-input-validation-states}

The phone input SHALL validate against the selected country's rules and SHALL reflect validity through the trailing icon and error message: neutral when empty, muted (no error icon) while partial or mid-typing, ✓ when complete and valid, and ✗ plus an error message when complete and invalid.

Scenario: Complete and valid shows check icon

  • GIVEN the selected country is US
  • WHEN the user enters the complete valid number +1 (415) 555-2671
  • THEN the trailing validation icon shows ✓
  • AND no error message is displayed

Scenario: Complete and invalid shows error

  • GIVEN the selected country is US
  • WHEN the user enters a complete but invalid number +1 (000) 000-0000
  • THEN the trailing validation icon shows ✗
  • AND an error message from specs/errors/user-facing-messages.kmd is displayed below the input

Scenario: Partial input stays muted

  • GIVEN the input is empty
  • WHEN the user types a partial number that is not yet complete
  • THEN no error icon is shown
  • AND no error message is displayed

Requirement: Keyboard navigation and Escape {#req-phone-input-keyboard-nav}

The phone input SHALL support keyboard operation of the country selector (arrow navigation, type-to-jump, Tab to move focus to the input) and Esc SHALL close the selector dropdown without changing the current selection.

Scenario: Escape closes selector without change

  • GIVEN the selected country is BR and the selector dropdown is open
  • WHEN the user presses Esc
  • THEN the dropdown closes
  • AND the selected country remains BR

Scenario: Tab moves focus from selector to input

  • GIVEN focus is on the country selector
  • WHEN the user presses Tab
  • THEN focus moves to the phone-number input

Requirement: Accessibility semantics {#req-phone-input-accessibility}

The phone input SHALL expose an accessible name for both the selector and the input, SHALL render the number field as <input type="tel" inputmode="tel">, and SHALL link its validation icon and error message to the input via aria-describedby.

Scenario: Input type and inputmode for mobile dial pad

  • GIVEN the phone input is rendered
  • WHEN the input element is inspected
  • THEN it has type="tel" and inputmode="tel"
  • AND it has an associated accessible label

Scenario: Error linked via aria-describedby

  • GIVEN the selected country is US and a complete invalid number is entered
  • WHEN the ✗ error message is displayed
  • THEN the input's aria-describedby references the element containing that error message

References