Phone Input React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React phone number input with a searchable country picker, formatting as you type, validation and one clean E.164 value out.
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.
Phone Input is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add phone-input, together with Select, Field, useLocale, Motion presets, useControllableState and 1 more and the npm packages libphonenumber-js, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add phone-inputPhone Input is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import PhoneInput from @/components/ui/phone-input.
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
defaultCountrydefault"RO"CountryCodeThe 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.
countrySelectdefaulttruebooleanfalse 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.
placeholderdefault""stringEmpty 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.
showValiditydefaultfalsebooleanA green check pops in (scale 0.8 to 1, snappy spring) while the number is valid, with a visually hidden label.
localedefault""stringThe 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.
labeldefault""ReactNodeRenders the field anatomy around the box, linked with htmlFor. Leave it empty for a bare field (then pass aria-label).
descriptiondefault""ReactNodeHelper text under the box, linked with aria-describedby.
errordefault""ReactNodeReplaces the description with the animated error and sets invalid (red ring, aria-invalid on the number input).
requireddefaultfalsebooleanThe red * after the label and required on the number input.
invaliddefaultfalsebooleanThe invalid look and aria-invalid without a message.
disableddefaultfalsebooleanDims to 50%, blocks typing and the country list, and keeps the hidden input out of the form.
readOnlydefaultfalsebooleanThe number stays visible and selectable, the list never opens, no hover tint.
sizedefault"md""sm" | "md" | "lg"32 / 40 / 48px boxes (md grows to 44px on touch screens), 16px text on phones so iOS never zooms.
variantdefault"soft""soft" | "outline" | "underline" | "ghost"The box look, shared with every field.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner radius per size (the button's scale); the flag button follows it. Ignored by underline.
responsivedefaulttruebooleanThe country list is a bottom sheet with search below 768px; false keeps the popover.
defaultOpendefaultfalsebooleanOpens the country list on mount. open / onOpenChange control it.
valuestringThe E.164 number ("+40712345678"), controlled. A value for another country switches the country. "" while empty or incomplete.
defaultValuedefault""stringThe starting number, uncontrolled: E.164 ("+40712345678", which also picks the country), or national digits for the default country.
onValueChange(e164: string, details: PhoneDetails) => voidFires 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 }.
countryCountryCodeThe selected country, controlled. Pair it with onCountryChange.
onCountryChange(country: CountryCode) => voidFires when the list, the typeahead on the flag button or a typed calling code changes the country.
countriesCountryCode[]Limits the list (and the countries a typed calling code may pick). All 245 by default, virtualized.
preferredCountriesdefault"["RO", "MD"]"CountryCode[]Shown first under the Suggested heading; an empty array drops the headings.
renderFlag(country: CountryCode) => ReactNodeCustom 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.
labelsPartial<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.
namedefault""stringRenders 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.
idstringThe number input's id (useId() by default; the flag button gets `${id}-country`).
containerHTMLElement | nullWhere the country list is portaled; document.body by default.
openbooleanThe country list's open state, controlled; with onOpenChange.
onOpenChange(open: boolean) => voidFires when the country list opens or closes.
autoCompletedefault"tel-national"stringOn the number input. A full number autofilled with tel ("+40 712…") is detected like a paste.
inputModedefault"tel""tel" | "numeric" | "decimal" | "text" | "email" | "url" | "search" | "none"The keypad on touch screens: tel has + and the digits; numeric only digits.
enterKeyHintdefault"next""next" | "done" | "go" | "send" | "search" | "enter" | "previous"The label of the on-screen Enter key; use done on the last field of a form.
fieldPropsPartial<FieldProps>Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel.
classNamesPartial<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.
refRefObject<HTMLInputElement | null> or (node) => voidThe 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
functionReads 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
functionBuilds 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
functionThe 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
typeThe ISO 3166-1 alpha-2 country codes libphonenumber-js knows, re-exported from it.
PhoneInputLabels
typeThe 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.
Dependencies
Files
- components/ui/phone-input.tsx
FAQ
Is the React Phone Input component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Phone Input?
Run npx wingo-ui@latest add phone-input. It also adds the Wingo UI items it builds on (Select, Field, useLocale, Motion presets, useControllableState and 1 more) and the npm packages libphonenumber-js, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/phone-input.
How does Phone Input behave on phones?
The country list is a bottom sheet with search below 768px; false keeps the popover. The responsive prop controls this and is on by default.
See also
Used in
More React input components
Articles using this component
- React Country Select With Flags, Search and Dial Codes
Component guides9 min read
- React Phone Input With Country Code and E.164 Value
Component guides8 min read
- React Form Components: Accessible Fields, Zod and Mobile
Component guides26 min read