Phone input (country selector + i18n format)
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.
Component — Phone input
*tatus* 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 localeaware 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, esES → 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; enLatn is not). - Manual override always allowed — user picks any country in the selector.
R3 — Format (mask + storage)
- Display: locale
aware mask via libphonenumberequivalent (e.g.+55 (11) 91234-5678for BR;+1 (415) 555-2671for US). - Storage: ISO E.164 (
+5511912345678). Always strip mask beforeemitting 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:
- *mpty* neutral (no icon)
- *artial / invalid format* muted (no error icon — too noisy mid-typing)
- *omplete + valid* ✓ icon
- *omplete + 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 |
Liveregion announce on country autoswitch (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 keyboardsshow 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 statesbeyond 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.