Phone Input
FreeA phone number field with a country selector (local flag assets, no remote images) that masks the NATIONAL number against a per-country numbering plan — the country code stays in the picker, where it is already shown. The bundled plans are a best effort rather than an authority: numbering plans change more often than a release cycle, so the length check is a hint and `plans` lets a host supply its own.
Also belongs under Data Display — it is catalogued in Forms & Inputs and stays there.
Complexity tier: Interactive The primitive bar, plus declared async states, a keyboard model, focus restoration and undo for destructive actions.
import { SvPhoneInputComponent } from '@styloviz/phone-input';Demo & usage
Basic usage
Usage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Default (US)
<sv-phone-input [(value)]="phone" />
Restrict the country list
<sv-phone-input [(value)]="phone" [(country)]="country" [countries]="['US', 'GB', 'CA']" />
International, with a starting country
<sv-phone-input [(value)]="phone" [(country)]="country" country="GB" />
Numbering plans change; a bundled table does not
Every non-NANP country used to share one mask — +# (###) ###-#### — which is the NANP shape with a plus bolted on: ONE country-code digit and TEN national ones. It was applied to the UK (+44), Australia (+61), Germany (+49), France (+33), India (+91) and Japan (+81). Not one of those codes fits in +#, and not one of those plans is ten digits, so picking "United Kingdom" and typing a real London number was impossible and nothing said so. The mask now covers the national number only — the picker already shows the dial code, and that was the slot the number needed. The bundled plans stay a best effort: pass `plans` to replace them with data you keep current (generated from libphonenumber, fetched, whatever), and the component stops pretending to own knowledge it cannot maintain.
<sv-phone-input [(value)]="phone" [(country)]="country" [plans]="myPlans()" /> // PhoneCountry: { code, name, dialCode, nationalDigits, groups?, nationalMask? } // nationalDigits is a number or [min, max]; groups is cosmetic.
A length hint, not a validity claim
When the digits fall outside the plan, the field says what it expected — "United Kingdom numbers are usually 9–10 digits; this has 6" — and leaves the judgement alone. That is deliberate: the plan may be out of date, which is the premise of the whole design, so a number the table calls short may be perfectly dialable. The one hard rule is E.164's ceiling of 15 digits including the country code, because that is the format's own definition and cannot go stale. For real validation, use a library that ships its own updates.
<sv-phone-input [(value)]="phone" (e164Changed)="store($event)" />
When to use
- Sign-up, checkout and profile forms that collect a phone number and benefit from an explicit country + matching mask.
- International products where a plain `tel` input leaves users guessing at the expected format.
- Domestic-only (US) forms — set `countries` to a single entry to hide the picker without losing the mask.
Async states
None — this component has nothing to load, so it has no loading, empty or error state to drive.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| value | string (two-way) | '' | Formatted phone value (two-way), forwarded straight from `sv-input-mask`. |
| country | string (two-way) | 'US' | Selected country, by ISO 3166-1 alpha-2 code (two-way). |
| countries | readonly string[] | null | null | Restrict the picker to a subset of the bundled countries. `null`/empty = show all. |
| plans | readonly PhoneCountry[] | null | null | Replace the bundled numbering data outright. THE ANSWER TO THE PART A BIGGER TABLE CANNOT FIX. Numbering plans change more often than a release cycle — new mobile prefixes, new area codes, whole plans renumbered — so a table compiled into a component is stale the day it ships. The bundled list is a best effort for eight countries; a host that needs the other two hundred, or needs today's data, passes its own in (generated from libphonenumber, fetched, whatever it keeps current) and the component stops pretending to own knowledge it cannot maintain. |
| size | InputMaskSize | 'md' | Field size, forwarded to `sv-input-mask`. |
| disabled | boolean | false | Disable the whole control. |
| label | string | 'Phone number' | Field label. |
| error | string | '' | Error message, forwarded to `sv-input-mask`. |
| extension | string (two-way) | '' | The extension, kept in its own field and OUT of the number. AN EXTENSION IS NOT PART OF A PHONE NUMBER. E.164 has no room for one — it is a country code plus a subscriber number and nothing else — so an extension typed into the main field corrupts the value that gets dialled and stored. Users type them anyway, because there is nowhere else to put them, which is exactly why the field has to exist. |
| showExtension | boolean | false | Show the extension field. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| rawChanged | string | Emitted with the raw (unformatted) digits, forwarded from `sv-input-mask`. |
| e164Changed | string | Emitted whenever the storable E.164 value changes. |
| valueChange | string | Emits the new `value` whenever it changes — the output half of `[(value)]`. |
| countryChange | string | Emits the new `country` whenever it changes — the output half of `[(country)]`. |
| extensionChange | string | Emits the new `extension` whenever it changes — the output half of `[(extension)]`. |
Customization & theming
- Wraps `@styloviz/forms`' `sv-input-mask`, so `size`, `disabled` and `error` all pass through unchanged.
- `countries` accepts an array of ISO 3166-1 alpha-2 codes to narrow the picker; omit it for the full bundled set.
- Switching country re-groups the digits and updates the dial code shown in the picker — it no longer throws the typed number away, because the country code was never among the digits.
- `plans` replaces the bundled numbering data outright; each entry carries `nationalDigits` (a number or `[min, max]`), an optional cosmetic `groups`, and an optional `nationalMask` for punctuation a group list cannot express.
- `svNationalMask()`, `svLengthHint()`, `svGroupNational()` and `SV_E164_MAX_DIGITS` are exported for building the same checks elsewhere.
- `showExtension` adds a separate digits-only extension field, two-way bound through `[(extension)]`. It is kept OUT of `e164Changed` — E.164 has no extension — so store it in its own column.
- A leading country code is only stripped when the value says it is there — it opens with `+` or `00`, or has more digits than the plan allows — so an Indian mobile such as 91987 65432 keeps its leading 91.
Best practices
- Do: Constrain `countries` to the markets you actually serve — a shorter list is faster to scan and search.
- Do: Read the raw digits (via `rawChanged`, forwarded from `sv-input-mask`) for storage/validation, not the formatted `value`.
- Tip: The flag assets are bundled SVGs, not remote images — this component works offline and behind strict CSPs.
- Don't: Don't assume the mask alone validates the number — it constrains formatting, not deliverability.
- Do: Pass `plans` if you serve more than the eight bundled countries, or if you need today's data. The table compiled into a component is stale the day it ships.
- Do: Store `e164Changed`, not the formatted value. The grouping is cosmetic and varies with the plan.
- Don't: Don't treat the length hint as validation. It reports what the bundled plan expected; a library that updates on its own schedule is what tells you whether a number is real.
Accessibility
- The country picker is a labelled button (`aria-haspopup="listbox"`) opening a keyboard-navigable listbox — arrow keys move, Enter selects, Escape closes, and typing a letter jumps to the first country with that name (repeat it to cycle).
- Each flag is decorative (`aria-hidden`); the accessible name for each option is the country name, not the flag.
- The underlying `sv-input-mask` keeps its own label/error association, so the field remains a single accessible text input.
Feedback on Phone Input
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.