Phone Input
International phone number input with country selector, flag emojis, and dial code search. Supports 50 countries with built-in formatting and validation.
Installation
npx shadcn@latest add @babelize/phone-inputUsage
"use client";
import { useState } from "react";
import { PhoneInput } from "@/components/ui/phone-input";
export function PhoneField() {
const [phone, setPhone] = useState("");
const [countryCode, setCountryCode] = useState("US");
return (
<PhoneInput
value={phone} // omit for uncontrolled
country={countryCode} // omit for uncontrolled (or use defaultCountry)
onValueChange={(value, country) => {
setPhone(value);
console.log(country.dialCode);
}}
onCountryChange={(country) => setCountryCode(country.code)}
placeholder="Enter phone number"
/>
);
}Country control
The country follows the same controlled/uncontrolled convention as the number:
pass country + onCountryChange to control it, or defaultCountry to seed
the uncontrolled default. When country is controlled, the parent has the final
say — honour onCountryChange if you want dial-code detection (below) to
switch countries.
Dial-code detection
Type or paste a number that starts with + and the component switches the
country to match, stripping the dial code and the separator right after it from
the field — the prefix the + declares outranks a manually picked country:
- Pasting
+44 20 7946 0958selects GB and leaves20 7946 0958in the field. - A
+typed in front of digits that are already in the field never strips them: the dial code is only consumed when its digits were entered after the+. Type+1in front of an existing number and the country switches to US once the1lands — and the number keeps every digit it had. - The longest matching dial code wins, so
+880…picks Bangladesh even if a shorter code also matches. - Shared dial codes keep the currently selected country when it is one of
the countries sharing the matched code, and otherwise pick the first one in
the offered list — so
+1never jumps away from US or CA mid-typing. The same rule covers any shared code in a customcountrieslist, such as+7if you offer both RU and KZ. - A dial code that is not in your
countrieslist is left alone — the country only ever switches to a country you actually offer. - After a switch, the country menu still works normally: picking a country
overrides the detected one, and nothing re-detects until the next time the
value starts with
+.
Props
| Prop | Type | Required | Description |
|---|---|---|---|
value | string | No | Phone number — pass this to control the component |
defaultValue | string | No | Initial phone number when uncontrolled |
onValueChange | (phone: string, country: Country) => void | No | Callback when phone number or country changes |
country | string | No | Selected country code (ISO 3166-1 alpha-2) — pass this to control the country |
onCountryChange | (country: Country) => void | No | Callback when the selected country changes — from the menu or dial-code detection |
defaultCountry | string | No | Initial country code when uncontrolled (ISO 3166-1 alpha-2, e.g. "US", "GB", "IN"; default: "US") |
countries | Country[] | No | Countries offered, in display order (default: COUNTRIES) |
placeholder | string | No | Placeholder text |
disabled | boolean | No | Disable the input |
showFlags | boolean | No | Show country flag next to dial code (default: true) |
size | "sm" | "default" | "lg" | No | Field height (default: "default") |
align | "start" | "end" | No | Edge of the field the menu lines up with (default: "start") |
side | "top" | "bottom" | No | Preferred menu side; flips when there is no room (default: "bottom") |
searchPlaceholder | string | No | Placeholder for the country search (default: "Search countries...") |
emptyMessage | ReactNode | No | Shown when the search matches nothing (default: "No countries found.") |
labels | Partial<PhoneInputLabels> | No | Override the built-in English UI text (see below) |
displayLocale | string | No | Locale for country names, e.g. "fr" (default: English names) |
maxHeight | number | string | No | Max height of the scrolling country list (default: "15rem") |
label | string | No | Accessible label for the input |
className | string | No | Classes for the root wrapper |
classNames | PhoneInputClassNames | No | Per-part classes: root, field, trigger, input, content, search, list, item, empty |
Any other prop — name, required, autoComplete, aria-invalid, data-* — is
spread onto the underlying <input>, and the ref is forwarded to it, so the
component works inside a plain HTML form. Setting aria-invalid turns the whole
field red, matching shadcn's Input.
Country Object
| Property | Type | Description |
|---|---|---|
code | string | ISO 3166-1 alpha-2 code (e.g. "US", "GB", "IN") |
dialCode | string | Dial code (e.g. "+1", "+44", "+91") |
name | string | Country name |
flag | string | Flag emoji |
Examples
With Babelize SDK
Pair Phone Input with the Babelize SDK for locale-aware validation:
"use client";
import { useEffect, useState } from "react";
import { PhoneInput } from "@/components/ui/phone-input";
import { useBabelize } from "@babelize/sdk/react";
export function ContactPhone() {
const { locale } = useBabelize();
const [phone, setPhone] = useState("");
const [countryCode, setCountryCode] = useState("US");
// The locale picks the country — until the user overrides it through the
// menu or by pasting a `+` number, which `onCountryChange` must honour.
useEffect(() => {
setCountryCode(locale === "en" ? "US" : locale === "de" ? "DE" : "GB");
}, [locale]);
return (
<PhoneInput
value={phone}
country={countryCode}
onCountryChange={(country) => setCountryCode(country.code)}
onValueChange={(value, country) => {
setPhone(value);
// Validate against locale-specific rules
}}
placeholder="Enter phone number"
/>
);
}With Form Validation
"use client";
import { useState } from "react";
import { PhoneInput } from "@/components/ui/phone-input";
export default function CheckoutForm() {
const [phone, setPhone] = useState("");
const [error, setError] = useState("");
const validate = (value: string) => {
if (value.length < 10) {
setError("Phone number is too short");
} else {
setError("");
}
};
return (
<div>
<PhoneInput
value={phone}
onValueChange={(value) => {
setPhone(value);
validate(value);
}}
defaultCountry="US"
placeholder="Enter phone number"
aria-invalid={error ? true : undefined}
/>
{error && <p className="mt-1 text-sm text-destructive">{error}</p>}
</div>
);
}With Country Filter
"use client";
import { COUNTRIES, PhoneInput } from "@/components/ui/phone-input";
// Only the markets you ship to, in the order you want them listed.
const SUPPORTED = ["US", "GB", "CA", "AU"].map((code) =>
COUNTRIES.find((country) => country.code === code)!,
);
export default function App() {
return (
<PhoneInput
countries={SUPPORTED}
defaultCountry="US"
onValueChange={(phone, country) => console.log(phone, country)}
/>
);
}Customizing parts
Every part takes its own classes through classNames, merged after the defaults:
<PhoneInput
size="lg"
align="end"
classNames={{
field: "rounded-full",
trigger: "ps-4",
content: "w-80",
item: "py-2",
}}
/>Translated UI text
Pass labels to translate the country menu's text, and displayLocale to show
country names in another language. Names come from the browser's
Intl.DisplayNames,
so their exact wording can vary slightly between browsers. Searching matches the
translated name, while onValueChange still reports the country's English name.
The searchPlaceholder and emptyMessage props still work, and win over labels
when both are set.
"use client";
import { PhoneInput } from "@/components/ui/phone-input";
export default function App() {
return (
<PhoneInput
defaultCountry="FR"
displayLocale="fr"
label="Numéro de téléphone"
placeholder="Numéro de téléphone"
labels={{
trigger: "Choisir un pays",
searchPlaceholder: "Rechercher un pays…",
searchLabel: "Rechercher un pays",
clearSearch: "Effacer la recherche",
emptyMessage: "Aucun pays trouvé.",
listbox: "Choisir un pays",
}}
/>
);
}| Key | Default | Used for |
|---|---|---|
trigger | "Select country" | Country trigger label |
searchPlaceholder | "Search countries..." | Search field placeholder |
searchLabel | "Search countries" | Search field accessible label |
clearSearch | "Clear search" | Clear button accessible label |
emptyMessage | "No countries found." | Shown when nothing matches |
listbox | "Select country" | Country list accessible label |
Keyboard support
The country menu follows the WAI-ARIA combobox pattern: focus stays in the search
field, which is exposed as a role="combobox", and the highlighted country is
announced through aria-activedescendant.
| Key | Action |
|---|---|
Enter, Space, or click | Open the country menu from the flag trigger |
ArrowDown / ArrowUp (closed) | Open with the first / last country highlighted |
ArrowDown / ArrowUp | Move the highlight, wrapping at both ends |
Home / End | Highlight the first / last country |
Enter | Select the highlighted country; focus moves to the number field |
Escape | Close the menu and return focus to the flag trigger |
| Typing | Filter by name, code, or dial code; the highlight restarts at the top |
The highlight stays on the row where it appeared when the menu opened — the
list scrolls beneath it, like a native menu — and the page itself never
scrolls. Pass maxHeight to change how tall the country list gets before it
scrolls.
Features
- 50 countries — pre-loaded with dial codes, flags, and names
- Dropdown search — filter countries by name or dial code
- Flag emojis — auto-detected from country code, with
showFlagstoggle - Keyboard accessible — arrow keys, Home / End, Enter, and Escape navigation
- ARIA labels —
role="combobox"search field witharia-activedescendant, plusaria-expanded,aria-haspopup, andaria-selected - Themeable — styled with shadcn/ui tokens (
border-input,bg-popover,ring-ring), so it follows your theme and dark mode - Animated menu — fades in and out with
motion, honouring reduced-motion settings, and flips upward when there is no room below - Controlled/uncontrolled — works with or without
valueandcountry - Dial-code detection — typing or pasting
+44…switches the country to GB and strips the prefix; shared codes (+1,+7) keep the current selection
Supported Countries
The input ships with 50 countries pre-loaded. Use the dropdown search to find any country by name or dial code. The full list includes:
| Country | Code | Dial Code | Flag |
|---|---|---|---|
| United States | US | +1 | 🇺🇸 |
| United Kingdom | GB | +44 | 🇬🇧 |
| Canada | CA | +1 | 🇨🇦 |
| Australia | AU | +61 | 🇦🇺 |
| India | IN | +91 | 🇮🇳 |
| Germany | DE | +49 | 🇩🇪 |
| France | FR | +33 | 🇫🇷 |
| Japan | JP | +81 | 🇯🇵 |
| Brazil | BR | +55 | 🇧🇷 |
| South Korea | KR | +82 | 🇰🇷 |
| ...and 40 more via the dropdown search |
Dependencies
This component uses:
clsxandtailwind-merge(bundled as dependencies)lucide-reactfor icons andmotionfor the menu animation
Peer dependencies:
react>= 18react-dom>= 18tailwindcss>= 3
Credits
Created by sapatmohit.