Phone Input
A country selector and a national number that together produce E.164 — the shape every telephony API wants. Honest about not being a validator.
src/components/ui/phone-input.tsxAbout
From the component's own source, where it was written.
A country selector and a national number that together produce E.164.
**The value is E.164** — +441632960961, digits and a leading plus, no spaces or brackets. That is what every telephony API, SMS gateway and database column wants, and storing the pretty version means every consumer re-parses it and they disagree about how.
**A mask input is not a substitute.** Phone formatting is per country and frequently per prefix inside a country: a fixed (___) ___-____ is wrong for most of the world and actively blocks valid numbers. Here the dial code is chosen explicitly and the national part is only lightly grouped.
**It does not claim to validate.** Real validation is a per-country length and prefix table that changes as regulators allocate ranges — that is libphonenumber, and it is 200 kB+. This checks that the national part is digits within a plausible length and says so, and onValidChange reports it without blocking typing. If you need to reject a number confidently, do it on the server where you can afford the library.
The country list is a prop with a small default, because the right list is usually "the countries you ship to", not all 249.
Install
npx astralyx-ui add phone-input
Usage
import { PhoneInput } from '@/components/ui/phone-input'
<PhoneInput value={phone} onChange={setPhone} defaultCountry="GB" />Composer
Change the props and the source below follows.
<PhoneInput size="md" value={phone} onChange={setPhone} />Examples
Producing E.164
<PhoneInput value={value} onChange={setValue} />API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
PhoneInput
Extends Omit<ComponentProps<'input'>, 'value' | 'defaultValue' | 'onChange' | 'size'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | +441632960961 — digits and a leading plus. Storing the pretty version means every consumer re-parses it and they disagree about how. |
| defaultValue | string | '' | — |
| onChange | (value: string) => void | — | — |
| onValidChange | (valid: boolean) => void | — | — |
| countries | Country[] | COMMON_COUNTRIES | A short default is exported as COMMON_COUNTRIES. The right list is usually "the countries you ship to", not all 249. |
| defaultCountry | string | 'US' | Selected when the value has no dial code to infer one from. |
| size | 'sm' | 'md' | 'lg' | 'md' | — |
| countryLabel | string | 'Country' | — |
| numberLabel | string | 'Phone number' | Accessible name for the number field. |
| invalid | boolean | — | — |
| autoComplete | tel-national | — | Set so a password manager can fill it, with inputMode="tel" for the phone keypad. |
Functions
| Function | Signature | Description |
|---|---|---|
| COMMON_COUNTRIES | Country[] | A short default. Pass countries for the list your product actually needs. |
Types
Country
A country selector and a national number that together produce E.164.
| Field | Type | Description |
|---|---|---|
| code | string | ISO 3166-1 alpha-2, uppercase. |
| name | string | — |
| dial | string | Dial code without the plus. |
| flag? | string | — |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| not a validator | stated plainly | Real validation is a per-country length and prefix table that changes as regulators allocate ranges. That is libphonenumber, and it is 200 kB+. This checks a plausible length and reports it through onValidChange without blocking typing. |
| vs MaskInput | why a mask fails | Formatting is per country and often per prefix inside one. A fixed (___) ___-____ is wrong for most of the world and blocks valid numbers. |