# Phone Input

> A React phone number input with a searchable country picker, formatting as you type, validation and one clean E.164 value out.

- Type: React component (Inputs)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add phone-input`
- Docs: https://wingo-ui.com/components/phone-input

## Interaction

Type the number and it formats itself the way the country writes it (712 345 678 for Romania, 69 123 456 for Moldova, 201 555 0123 for the US), with the caret kept after the digit you typed; Backspace on a space deletes the digit before it. A leading trunk 0 is dropped (0712 345 678 becomes 712 345 678), and digits beyond the country's longest number are refused. Type or paste a number that starts with + or 00 and the calling code picks the country: +373 69… switches the flag to Moldova, 0040 712… to Romania; browser autofill of a full number works the same way. The flag button (flag, +40 and a chevron) opens the country list: search by the localized name, the English name, the ISO code or the calling code, suggested countries first. With the keyboard: Tab reaches the flag button, then the number. On the flag button Enter, Space or ArrowDown opens the list and a letter jumps to the next country starting with it without opening; in the list the search is focused, arrows, Home / End and PageUp / PageDown move, Enter picks and puts focus back in the number, Escape closes and returns to the flag button. With showValidity a green check pops in once the number is valid. On a phone the list is a bottom sheet with search and 48px rows, the field raises the phone keypad, and the flag button keeps a 44px hit area.

## Installation

```bash
npx wingo-ui@latest add phone-input
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/phone-input` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/phone-input.tsx`

npm dependencies: `npm i libphonenumber-js motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { PhoneInput, type PhoneDetails } from "@/components/ui/phone-input"

export function ContactForm() {
  const [phone, setPhone] = useState("")
  const [details, setDetails] = useState<PhoneDetails | null>(null)
  const [touched, setTouched] = useState(false)

  return (
    <form className="flex flex-col gap-4">
      <PhoneInput
        name="phone"
        label="Phone"
        description="Only interested buyers will call you."
        defaultCountry="US"
        preferredCountries={["US", "CA", "GB"]}
        value={phone}
        onValueChange={(e164, info) => {
          setPhone(e164)
          setDetails(info)
        }}
        onBlur={() => setTouched(true)}
        error={touched && details?.national && !details.valid ? "The phone number is not valid" : undefined}
        showValidity
        required
      />
    </form>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultCountry?` | `CountryCode` | `"RO"` | The starting country (ISO 3166 code such as US, GB or RO) when the value does not name one. An unknown code falls back to RO. |
| `countrySelect?` | `boolean` | `true` | false fixes the country: no picker, the calling code shows as a text prefix ("+40"); a pasted "+40 712…" or "0040 712…" still fills in the national digits. |
| `placeholder?` | `string` | `""` | Empty shows the country's example mobile number from libphonenumber-js (712 034 567 for RO), formatted like the field formats and without the calling code; it follows the country. |
| `showValidity?` | `boolean` | `false` | A green check pops in (scale 0.8 to 1, snappy spring) while the number is valid, with a visually hidden label. |
| `locale?` | `string` | `""` | The language of the country names (Intl.DisplayNames) and of the list order, e.g. "en-US" or "de-DE". Empty uses the UIProvider locale, then the nearest LocaleProvider, then the page. English names stay searchable in every locale. |
| `label?` | `ReactNode` | `""` | Renders the field anatomy around the box, linked with htmlFor. Leave it empty for a bare field (then pass aria-label). |
| `description?` | `ReactNode` | `""` | Helper text under the box, linked with aria-describedby. |
| `error?` | `ReactNode` | `""` | Replaces the description with the animated error and sets invalid (red ring, aria-invalid on the number input). |
| `required?` | `boolean` | `false` | The red * after the label and required on the number input. |
| `invalid?` | `boolean` | `false` | The invalid look and aria-invalid without a message. |
| `disabled?` | `boolean` | `false` | Dims to 50%, blocks typing and the country list, and keeps the hidden input out of the form. |
| `readOnly?` | `boolean` | `false` | The number stays visible and selectable, the list never opens, no hover tint. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | 32 / 40 / 48px boxes (md grows to 44px on touch screens), 16px text on phones so iOS never zooms. |
| `variant?` | `"soft" \| "outline" \| "underline" \| "ghost"` | `"soft"` | The box look, shared with every field. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner radius per size (the button's scale); the flag button follows it. Ignored by underline. |
| `responsive?` | `boolean` | `true` | The country list is a bottom sheet with search below 768px; false keeps the popover. |
| `defaultOpen?` | `boolean` | `false` | Opens the country list on mount. open / onOpenChange control it. |
| `value?` | `string` |  | The E.164 number ("+40712345678"), controlled. A value for another country switches the country. "" while empty or incomplete. |
| `defaultValue?` | `string` | `""` | The starting number, uncontrolled: E.164 ("+40712345678", which also picks the country), or national digits for the default country. |
| `onValueChange?` | `(e164: string, details: PhoneDetails) => void` |  | Fires on every edit: the E.164 number once it is possible, else "" with the details, so a form can say incomplete. PhoneDetails = { country?: CountryCode; national: string; international: string; valid: boolean; possible: boolean }. |
| `country?` | `CountryCode` |  | The selected country, controlled. Pair it with onCountryChange. |
| `onCountryChange?` | `(country: CountryCode) => void` |  | Fires when the list, the typeahead on the flag button or a typed calling code changes the country. |
| `countries?` | `CountryCode[]` |  | Limits the list (and the countries a typed calling code may pick). All 245 by default, virtualized. |
| `preferredCountries?` | `CountryCode[]` | `"[\"RO\", \"MD\"]"` | Shown first under the Suggested heading; an empty array drops the headings. |
| `renderFlag?` | `(country: CountryCode) => ReactNode` |  | Custom flags (SVGs from your app) in the button and the list. By default the emoji flag; on Windows, which has no flag emoji, the ISO code in a small chip after mount. |
| `labels?` | `Partial<PhoneInputLabels>` |  | Every visible and spoken string: the flag button's name ("Country: Romania +40") and the sheet title, the search placeholder, the empty text, the group headings, the example format read after the field and the validity text. |
| `name?` | `string` | `""` | Renders a hidden input with this name and the E.164 value, so FormData gets the clean number (disabled fields are not submitted). The readout under the demo shows the pair. |
| `id?` | `string` |  | The number input's id (useId() by default; the flag button gets `${id}-country`). |
| `container?` | `HTMLElement \| null` |  | Where the country list is portaled; document.body by default. |
| `open?` | `boolean` |  | The country list's open state, controlled; with onOpenChange. |
| `onOpenChange?` | `(open: boolean) => void` |  | Fires when the country list opens or closes. |
| `autoComplete?` | `string` | `"tel-national"` | On the number input. A full number autofilled with tel ("+40 712…") is detected like a paste. |
| `inputMode?` | `"tel" \| "numeric" \| "decimal" \| "text" \| "email" \| "url" \| "search" \| "none"` | `"tel"` | The keypad on touch screens: tel has + and the digits; numeric only digits. |
| `enterKeyHint?` | `"next" \| "done" \| "go" \| "send" \| "search" \| "enter" \| "previous"` | `"next"` | The label of the on-screen Enter key; use done on the last field of a form. |
| `fieldProps?` | `Partial<FieldProps>` |  | Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel. |
| `classNames?` | `Partial<Record<"field" \| "label" \| "description" \| "error" \| "counter" \| "box" \| "country" \| "flag" \| "code" \| "chevron" \| "input" \| "validity" \| "content", string>>` |  | Extra classes for the field parts, the box, the flag button and its parts, the number input, the check and the list. |
| `ref?` | `RefObject<HTMLInputElement \| null> or (node) => void` |  | The number input. Every other native prop lands on it too: onBlur, autoComplete ("tel-national"), inputMode ("tel"), enterKeyHint ("next"), aria-*. The input holds formatted national digits, so with react-hook-form use Controller (value / onValueChange). The default metadata is libphonenumber-js min (lengths and leading digits); import from libphonenumber-js/max in your copy for stricter number-type checks at a larger size. |

## Parts

### parsePhoneValue (function)

Reads any phone value ("+40712345678", "0040 712…", or national digits like "0712 345 678" for the given country) into the E.164 number and its details, for validating server data or a form default. Signature: (value: string, country?: CountryCode) => { e164: string; details: PhoneDetails }.

### getPhoneDetails (function)

Builds the E.164 number and the details from a country and its national digits; e164 is "" until the number is possible. Signature: (country: CountryCode, digits: string) => { e164: string; details: PhoneDetails }.

### countryFlag (function)

The emoji flag for an ISO country code, from regional indicator letters: countryFlag("US") is the US flag. Signature: (country: string) => string.

### PhoneDetails (type)

{ country?: CountryCode; national: string; international: string; valid: boolean; possible: boolean }, passed to onValueChange next to the E.164 value.

### CountryCode (type)

The ISO 3166-1 alpha-2 country codes libphonenumber-js knows, re-exported from it.

### PhoneInputLabels (type)

The strings of the country list (country, search, empty, suggested, allCountries, example, valid) passed through the labels prop.

## Examples

- **Preferred countries**: Romania, Moldova, Germany, Italy, Spain and the UK first under Suggested, then every country A to Z.
- **Fixed country**: countrySelect={false}: no picker, "+44" as a text prefix; a trunk 0 is still dropped.
- **Paste detection**: Pasting +373… switches to Moldova, 0040… to Romania, and 0722… keeps the country and drops the 0.
- **Validity check**: showValidity pops a green check once the number is valid; the second field shows the error state.
- **Sizes and variants**: sm, md outline, lg with a full radius and underline, each with a number from another country.
- **Country names by locale**: locale="ro-RO" next to locale="en-GB": the names and their order follow the locale.

## See also

- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Masked Input](https://wingo-ui.com/components/masked-input) ([Markdown](https://wingo-ui.com/components/masked-input.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))

---

Canonical: https://wingo-ui.com/components/phone-input
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
