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.tsx

About

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.

Props
tsx
<PhoneInput size="md" value={phone} onChange={setPhone} />

Examples

Producing E.164

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'>.

PropTypeDefaultDescription
valuestring—+441632960961 — digits and a leading plus. Storing the pretty version means every consumer re-parses it and they disagree about how.
defaultValuestring''—
onChange(value: string) => void——
onValidChange(valid: boolean) => void——
countriesCountry[]COMMON_COUNTRIESA short default is exported as COMMON_COUNTRIES. The right list is usually "the countries you ship to", not all 249.
defaultCountrystring'US'Selected when the value has no dial code to infer one from.
size'sm' | 'md' | 'lg''md'—
countryLabelstring'Country'—
numberLabelstring'Phone number'Accessible name for the number field.
invalidboolean——
autoCompletetel-national—Set so a password manager can fill it, with inputMode="tel" for the phone keypad.

Functions

FunctionSignatureDescription
COMMON_COUNTRIESCountry[]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.

FieldTypeDescription
codestringISO 3166-1 alpha-2, uppercase.
namestring—
dialstringDial code without the plus.
flag?string—

Behaviour

TopicDetailDescription
not a validatorstated plainlyReal 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 MaskInputwhy a mask failsFormatting is per country and often per prefix inside one. A fixed (___) ___-____ is wrong for most of the world and blocks valid numbers.