Babelize Elements
Inputs

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-input

Usage

"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 0958 selects GB and leaves 20 7946 0958 in 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 +1 in front of an existing number and the country switches to US once the 1 lands — 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 +1 never jumps away from US or CA mid-typing. The same rule covers any shared code in a custom countries list, such as +7 if you offer both RU and KZ.
  • A dial code that is not in your countries list 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

PropTypeRequiredDescription
valuestringNoPhone number — pass this to control the component
defaultValuestringNoInitial phone number when uncontrolled
onValueChange(phone: string, country: Country) => voidNoCallback when phone number or country changes
countrystringNoSelected country code (ISO 3166-1 alpha-2) — pass this to control the country
onCountryChange(country: Country) => voidNoCallback when the selected country changes — from the menu or dial-code detection
defaultCountrystringNoInitial country code when uncontrolled (ISO 3166-1 alpha-2, e.g. "US", "GB", "IN"; default: "US")
countriesCountry[]NoCountries offered, in display order (default: COUNTRIES)
placeholderstringNoPlaceholder text
disabledbooleanNoDisable the input
showFlagsbooleanNoShow country flag next to dial code (default: true)
size"sm" | "default" | "lg"NoField height (default: "default")
align"start" | "end"NoEdge of the field the menu lines up with (default: "start")
side"top" | "bottom"NoPreferred menu side; flips when there is no room (default: "bottom")
searchPlaceholderstringNoPlaceholder for the country search (default: "Search countries...")
emptyMessageReactNodeNoShown when the search matches nothing (default: "No countries found.")
labelsPartial<PhoneInputLabels>NoOverride the built-in English UI text (see below)
displayLocalestringNoLocale for country names, e.g. "fr" (default: English names)
maxHeightnumber | stringNoMax height of the scrolling country list (default: "15rem")
labelstringNoAccessible label for the input
classNamestringNoClasses for the root wrapper
classNamesPhoneInputClassNamesNoPer-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

PropertyTypeDescription
codestringISO 3166-1 alpha-2 code (e.g. "US", "GB", "IN")
dialCodestringDial code (e.g. "+1", "+44", "+91")
namestringCountry name
flagstringFlag 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",
      }}
    />
  );
}
KeyDefaultUsed 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.

KeyAction
Enter, Space, or clickOpen the country menu from the flag trigger
ArrowDown / ArrowUp (closed)Open with the first / last country highlighted
ArrowDown / ArrowUpMove the highlight, wrapping at both ends
Home / EndHighlight the first / last country
EnterSelect the highlighted country; focus moves to the number field
EscapeClose the menu and return focus to the flag trigger
TypingFilter 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 showFlags toggle
  • Keyboard accessible — arrow keys, Home / End, Enter, and Escape navigation
  • ARIA labels — role="combobox" search field with aria-activedescendant, plus aria-expanded, aria-haspopup, and aria-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 value and country
  • 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:

CountryCodeDial CodeFlag
United StatesUS+1🇺🇸
United KingdomGB+44🇬🇧
CanadaCA+1🇨🇦
AustraliaAU+61🇦🇺
IndiaIN+91🇮🇳
GermanyDE+49🇩🇪
FranceFR+33🇫🇷
JapanJP+81🇯🇵
BrazilBR+55🇧🇷
South KoreaKR+82🇰🇷
...and 40 more via the dropdown search

Dependencies

This component uses:

  • clsx and tailwind-merge (bundled as dependencies)
  • lucide-react for icons and motion for the menu animation

Peer dependencies:

  • react >= 18
  • react-dom >= 18
  • tailwindcss >= 3

Credits

Created by sapatmohit.

On this page