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
- Pick a phone input component for a Koder product form
All triggers
- Add a phone field to a form
- Replace a plain <input type='tel'> with a validated, i18n-aware control
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-5678for BR;+1 (415) 555-2671for 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
| Key | Action |
|---|---|
| Tab into selector | Open dropdown / focus current selection |
| ↑ / ↓ in selector | Navigate countries |
| Type letters in selector | Jump to country starting with those letters |
| Tab out of selector | Focus phone input |
| Type / paste in input | Mask applies live |
| Tab out of input | Validate + show error if invalid |
| Esc in selector | Close 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"perspecs/components/combobox.kmdR3. - 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-XXXXeven 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
specs/components/combobox.kmdspecs/i18n/contract.kmdspecs/testing/ouia-test-hooks.kmd