Phone Input
An international phone input component with country selection, smart validation, country-specific placeholders, and visual feedback for global phone numbers.
A comprehensive phone input component with international country selection, smart validation with country-specific patterns, dynamic placeholders, and a clean borderless design for global applications.
function BasicPhoneInputExample() {
const [phone, setPhone] = React.useState("");
return (
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
placeholder="Enter your phone number"
/>
);
}
Installation
Install following dependencies:
npm install class-variance-authority lucide-react
Copy and paste the following code into your project.
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { ChevronDown, Phone, CheckCircle, XCircle } from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
// Country data with phone codes, patterns, and placeholders
const countries = [
{
code: "US",
name: "United States",
flag: "🇺🇸",
phoneCode: "+1",
placeholder: "(555) 123-4567",
pattern: /^(\([0-9]{3}\))\s?[0-9]{3}-?[0-9]{4}$/,
maxLength: 14,
},
{
code: "GB",
name: "United Kingdom",
flag: "🇬🇧",
phoneCode: "+44",
placeholder: "7911 123456",
pattern: /^[0-9]{10,11}$/,
maxLength: 11,
},
{
code: "CA",
name: "Canada",
flag: "🇨🇦",
phoneCode: "+1",
placeholder: "(555) 123-4567",
pattern: /^(\([0-9]{3}\))\s?[0-9]{3}-?[0-9]{4}$/,
maxLength: 14,
},
{
code: "AU",
name: "Australia",
flag: "🇦🇺",
phoneCode: "+61",
placeholder: "412 345 678",
pattern: /^[0-9]{9,10}$/,
maxLength: 11,
},
{
code: "DE",
name: "Germany",
flag: "🇩🇪",
phoneCode: "+49",
placeholder: "151 12345678",
pattern: /^[0-9]{10,12}$/,
maxLength: 12,
},
{
code: "FR",
name: "France",
flag: "🇫🇷",
phoneCode: "+33",
placeholder: "6 12 34 56 78",
pattern: /^[0-9]{10}$/,
maxLength: 12,
},
{
code: "IT",
name: "Italy",
flag: "🇮🇹",
phoneCode: "+39",
placeholder: "312 345 6789",
pattern: /^[0-9]{9,10}$/,
maxLength: 11,
},
{
code: "ES",
name: "Spain",
flag: "🇪🇸",
phoneCode: "+34",
placeholder: "612 34 56 78",
pattern: /^[0-9]{9}$/,
maxLength: 11,
},
{
code: "NL",
name: "Netherlands",
flag: "🇳🇱",
phoneCode: "+31",
placeholder: "6 12345678",
pattern: /^[0-9]{9}$/,
maxLength: 10,
},
{
code: "BE",
name: "Belgium",
flag: "🇧🇪",
phoneCode: "+32",
placeholder: "470 12 34 56",
pattern: /^[0-9]{9}$/,
maxLength: 11,
},
{
code: "CH",
name: "Switzerland",
flag: "🇨🇭",
phoneCode: "+41",
placeholder: "78 123 45 67",
pattern: /^[0-9]{9}$/,
maxLength: 11,
},
{
code: "AT",
name: "Austria",
flag: "🇦🇹",
phoneCode: "+43",
placeholder: "664 123456",
pattern: /^[0-9]{10,11}$/,
maxLength: 12,
},
{
code: "SE",
name: "Sweden",
flag: "🇸🇪",
phoneCode: "+46",
placeholder: "70 123 45 67",
pattern: /^[0-9]{9}$/,
maxLength: 11,
},
{
code: "NO",
name: "Norway",
flag: "🇳🇴",
phoneCode: "+47",
placeholder: "412 34 567",
pattern: /^[0-9]{8}$/,
maxLength: 8,
},
{
code: "DK",
name: "Denmark",
flag: "🇩🇰",
phoneCode: "+45",
placeholder: "20 12 34 56",
pattern: /^[0-9]{8}$/,
maxLength: 8,
},
{
code: "FI",
name: "Finland",
flag: "🇫🇮",
phoneCode: "+358",
placeholder: "50 123 4567",
pattern: /^[0-9]{9,10}$/,
maxLength: 11,
},
{
code: "PL",
name: "Poland",
flag: "🇵🇱",
phoneCode: "+48",
placeholder: "512 123 456",
pattern: /^[0-9]{9}$/,
maxLength: 11,
},
{
code: "CZ",
name: "Czech Republic",
flag: "🇨🇿",
phoneCode: "+420",
placeholder: "601 123 456",
pattern: /^[0-9]{9}$/,
maxLength: 9,
},
{
code: "HU",
name: "Hungary",
flag: "🇭🇺",
phoneCode: "+36",
placeholder: "20 123 4567",
pattern: /^[0-9]{8,9}$/,
maxLength: 10,
},
{
code: "PT",
name: "Portugal",
flag: "🇵🇹",
phoneCode: "+351",
placeholder: "912 345 678",
pattern: /^[0-9]{9}$/,
maxLength: 9,
},
{
code: "GR",
name: "Greece",
flag: "🇬🇷",
phoneCode: "+30",
placeholder: "694 123 4567",
pattern: /^[0-9]{10}$/,
maxLength: 10,
},
{
code: "TR",
name: "Turkey",
flag: "🇹🇷",
phoneCode: "+90",
placeholder: "532 123 45 67",
pattern: /^[0-9]{10}$/,
maxLength: 12,
},
{
code: "RU",
name: "Russia",
flag: "🇷🇺",
phoneCode: "+7",
placeholder: "912 123-45-67",
pattern: /^[0-9]{10}$/,
maxLength: 12,
},
{
code: "JP",
name: "Japan",
flag: "🇯🇵",
phoneCode: "+81",
placeholder: "90 1234 5678",
pattern: /^[0-9]{10,11}$/,
maxLength: 13,
},
{
code: "KR",
name: "South Korea",
flag: "🇰🇷",
phoneCode: "+82",
placeholder: "10 1234 5678",
pattern: /^[0-9]{10,11}$/,
maxLength: 13,
},
{
code: "CN",
name: "China",
flag: "🇨🇳",
phoneCode: "+86",
placeholder: "138 0013 8000",
pattern: /^[0-9]{11}$/,
maxLength: 13,
},
{
code: "IN",
name: "India",
flag: "🇮🇳",
phoneCode: "+91",
placeholder: "98765 43210",
pattern: /^[0-9]{10}$/,
maxLength: 11,
},
{
code: "SG",
name: "Singapore",
flag: "🇸🇬",
phoneCode: "+65",
placeholder: "8123 4567",
pattern: /^[0-9]{8}$/,
maxLength: 8,
},
{
code: "MY",
name: "Malaysia",
flag: "🇲🇾",
phoneCode: "+60",
placeholder: "12-345 6789",
pattern: /^[0-9]{9,10}$/,
maxLength: 12,
},
{
code: "TH",
name: "Thailand",
flag: "🇹🇭",
phoneCode: "+66",
placeholder: "81 234 5678",
pattern: /^[0-9]{9}$/,
maxLength: 11,
},
{
code: "ID",
name: "Indonesia",
flag: "🇮🇩",
phoneCode: "+62",
placeholder: "812-3456-789",
pattern: /^[0-9]{9,13}$/,
maxLength: 15,
},
{
code: "PH",
name: "Philippines",
flag: "🇵🇭",
phoneCode: "+63",
placeholder: "917 123 4567",
pattern: /^[0-9]{10}$/,
maxLength: 12,
},
{
code: "VN",
name: "Vietnam",
flag: "🇻🇳",
phoneCode: "+84",
placeholder: "91 234 56 78",
pattern: /^[0-9]{9,10}$/,
maxLength: 12,
},
{
code: "BD",
name: "Bangladesh",
flag: "🇧🇩",
phoneCode: "+880",
placeholder: "1712-345678",
pattern: /^[0-9]{10,11}$/,
maxLength: 13,
},
{
code: "PK",
name: "Pakistan",
flag: "🇵🇰",
phoneCode: "+92",
placeholder: "301 2345678",
pattern: /^[0-9]{10}$/,
maxLength: 12,
},
{
code: "LK",
name: "Sri Lanka",
flag: "🇱🇰",
phoneCode: "+94",
placeholder: "71 234 5678",
pattern: /^[0-9]{9}$/,
maxLength: 10,
},
{
code: "AE",
name: "United Arab Emirates",
flag: "🇦🇪",
phoneCode: "+971",
placeholder: "50 123 4567",
pattern: /^[0-9]{9}$/,
maxLength: 9,
},
{
code: "SA",
name: "Saudi Arabia",
flag: "🇸🇦",
phoneCode: "+966",
placeholder: "50 123 4567",
pattern: /^[0-9]{9}$/,
maxLength: 9,
},
{
code: "IL",
name: "Israel",
flag: "🇮🇱",
phoneCode: "+972",
placeholder: "50-123-4567",
pattern: /^[0-9]{9}$/,
maxLength: 11,
},
{
code: "EG",
name: "Egypt",
flag: "🇪🇬",
phoneCode: "+20",
placeholder: "10 1234 5678",
pattern: /^[0-9]{10}$/,
maxLength: 12,
},
{
code: "ZA",
name: "South Africa",
flag: "🇿🇦",
phoneCode: "+27",
placeholder: "82 123 4567",
pattern: /^[0-9]{9}$/,
maxLength: 10,
},
{
code: "NG",
name: "Nigeria",
flag: "🇳🇬",
phoneCode: "+234",
placeholder: "802 123 4567",
pattern: /^[0-9]{10}$/,
maxLength: 10,
},
{
code: "KE",
name: "Kenya",
flag: "🇰🇪",
phoneCode: "+254",
placeholder: "712 123456",
pattern: /^[0-9]{9}$/,
maxLength: 9,
},
{
code: "GH",
name: "Ghana",
flag: "🇬🇭",
phoneCode: "+233",
placeholder: "23 123 4567",
pattern: /^[0-9]{9}$/,
maxLength: 10,
},
{
code: "BR",
name: "Brazil",
flag: "🇧🇷",
phoneCode: "+55",
placeholder: "(11) 91234-5678",
pattern: /^[0-9]{10,11}$/,
maxLength: 15,
},
{
code: "MX",
name: "Mexico",
flag: "🇲🇽",
phoneCode: "+52",
placeholder: "55 1234 5678",
pattern: /^[0-9]{10}$/,
maxLength: 12,
},
{
code: "AR",
name: "Argentina",
flag: "🇦🇷",
phoneCode: "+54",
placeholder: "9 11 1234-5678",
pattern: /^[0-9]{10,11}$/,
maxLength: 14,
},
{
code: "CL",
name: "Chile",
flag: "🇨🇱",
phoneCode: "+56",
placeholder: "9 8765 4321",
pattern: /^[0-9]{9}$/,
maxLength: 11,
},
{
code: "CO",
name: "Colombia",
flag: "🇨🇴",
phoneCode: "+57",
placeholder: "321 1234567",
pattern: /^[0-9]{10}$/,
maxLength: 12,
},
{
code: "PE",
name: "Peru",
flag: "🇵🇪",
phoneCode: "+51",
placeholder: "987 654 321",
pattern: /^[0-9]{9}$/,
maxLength: 11,
},
{
code: "VE",
name: "Venezuela",
flag: "🇻🇪",
phoneCode: "+58",
placeholder: "412-1234567",
pattern: /^[0-9]{10}$/,
maxLength: 12,
},
{
code: "UY",
name: "Uruguay",
flag: "🇺🇾",
phoneCode: "+598",
placeholder: "91 123 456",
pattern: /^[0-9]{8}$/,
maxLength: 8,
},
].sort((a, b) => a.name.localeCompare(b.name));
// Phone validation utility
const validatePhoneNumber = (
phoneNumber: string,
countryCode: string
): boolean => {
const country = countries.find((c) => c.code === countryCode);
if (!country) return false;
// Remove spaces, dashes, parentheses for validation
const cleanNumber = phoneNumber.replace(/[\s\-\(\)]/g, "");
return country.pattern.test(cleanNumber);
};
const phoneInputVariants = cva(
"flex w-full items-center gap-2 bg-transparent text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
{
variants: {
variant: {
default: "",
outline: "",
ghost: "",
},
size: {
sm: "h-7 sm:h-8 px-2 text-xs",
default: "h-8 sm:h-9 px-2 sm:px-3 text-xs sm:text-sm",
lg: "h-12 sm:h-10 px-3 sm:px-4 text-sm",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface PhoneInputProps
extends VariantProps<typeof phoneInputVariants> {
value?: string;
onChange?: (
value: string,
formattedValue: string,
countryCode: string,
isValid?: boolean
) => void;
placeholder?: string;
className?: string;
disabled?: boolean;
defaultCountry?: string;
showFlag?: boolean;
showIcon?: boolean;
error?: boolean;
showValidation?: boolean;
onValidationChange?: (isValid: boolean) => void;
}
export function PhoneInput({
value = "",
onChange,
placeholder,
className,
disabled = false,
defaultCountry = "US",
showFlag = true,
showIcon = true,
error = false,
showValidation = false,
onValidationChange,
variant,
size,
...props
}: PhoneInputProps) {
const [selectedCountry, setSelectedCountry] = React.useState(
countries.find((c) => c.code === defaultCountry) || countries[0]
);
const [phoneNumber, setPhoneNumber] = React.useState("");
const [isValid, setIsValid] = React.useState(false);
// Use country-specific placeholder if none provided
const effectivePlaceholder = placeholder || selectedCountry.placeholder;
React.useEffect(() => {
if (value) {
// If value includes country code, try to parse it
const countryMatch = countries.find((c) => value.startsWith(c.phoneCode));
if (countryMatch) {
setSelectedCountry(countryMatch);
setPhoneNumber(value.slice(countryMatch.phoneCode.length).trim());
} else {
setPhoneNumber(value);
}
}
}, [value]);
// Validate phone number whenever it changes
React.useEffect(() => {
const valid =
phoneNumber.length > 0
? validatePhoneNumber(phoneNumber, selectedCountry.code)
: false;
setIsValid(valid);
onValidationChange?.(valid);
}, [phoneNumber, selectedCountry.code, onValidationChange]);
const handleCountryChange = (countryCode: string) => {
const country = countries.find((c) => c.code === countryCode);
if (country) {
setSelectedCountry(country);
const formattedValue = `${country.phoneCode}${
phoneNumber ? ` ${phoneNumber}` : ""
}`;
const valid =
phoneNumber.length > 0
? validatePhoneNumber(phoneNumber, country.code)
: false;
onChange?.(phoneNumber, formattedValue, country.code, valid);
}
};
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let newValue = e.target.value.replace(/[^\d\s\-\(\)]/g, "");
// Apply max length limit based on country
if (
selectedCountry.maxLength &&
newValue.length > selectedCountry.maxLength
) {
newValue = newValue.slice(0, selectedCountry.maxLength);
}
setPhoneNumber(newValue);
const formattedValue = `${selectedCountry.phoneCode}${
newValue ? ` ${newValue}` : ""
}`;
const valid =
newValue.length > 0
? validatePhoneNumber(newValue, selectedCountry.code)
: false;
onChange?.(newValue, formattedValue, selectedCountry.code, valid);
};
return (
<div
className={cn(phoneInputVariants({ variant, size }), className)}
{...props}
>
{showIcon && <Phone className="h-4 w-4 shrink-0 text-muted-foreground" />}
<div className="flex items-center gap-1 shrink-0">
<Select
value={selectedCountry.code}
onValueChange={handleCountryChange}
disabled={disabled}
>
<SelectTrigger className="h-auto border-none bg-transparent p-0 focus:ring-0 focus-visible:ring-transparent focus-visible:border-transparent focus-visible:outline-transparent active:ring-transparent active:border-transparent active:outline-transparent focus:ring-transparent focus:border-transparent focus:outline-transparent">
<SelectValue>
<div className="flex items-center gap-1">
{showFlag && (
<span className="text-sm">{selectedCountry.flag}</span>
)}
<span className="text-xs text-muted-foreground">
{selectedCountry.phoneCode}
</span>
</div>
</SelectValue>
</SelectTrigger>
<SelectContent className="max-h-60">
{countries.map((country) => (
<SelectItem key={country.code} value={country.code}>
<div className="flex items-center gap-2">
<span>{country.flag}</span>
<span className="font-medium">{country.name}</span>
<span className="text-xs text-muted-foreground">
{country.phoneCode}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Input
type="tel"
value={phoneNumber}
onChange={handlePhoneChange}
placeholder={effectivePlaceholder}
disabled={disabled}
className={cn(
"border-none bg-transparent p-0 focus-visible:ring-transparent focus-visible:border-transparent focus-visible:outline-transparent",
showValidation &&
phoneNumber.length > 0 &&
(isValid ? "text-green-600" : "text-red-600")
)}
maxLength={selectedCountry.maxLength}
/>
{showValidation && phoneNumber.length > 0 && (
<div className="ml-auto shrink-0">
{isValid ? (
<CheckCircle className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-red-600" />
)}
</div>
)}
</div>
);
}
// Example Components for Documentation
export function BasicPhoneInputExample() {
const [phone, setPhone] = React.useState("");
return (
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
placeholder="Enter your phone number"
/>
);
}
export function PhoneInputVariantsExample() {
const [phone, setPhone] = React.useState("");
return (
<div className="flex flex-col gap-4">
<div>
<label className="mb-2 block text-sm font-medium">Default</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
variant="default"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Outline</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
variant="outline"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Ghost</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
variant="ghost"
/>
</div>
</div>
);
}
export function PhoneInputSizesExample() {
const [phone, setPhone] = React.useState("");
return (
<div className="flex flex-col gap-4">
<div>
<label className="mb-2 block text-sm font-medium">Small</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
size="sm"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Default</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
size="default"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Large</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
size="lg"
/>
</div>
</div>
);
}
export function PhoneInputWithoutFlagExample() {
const [phone, setPhone] = React.useState("");
return (
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
showFlag={false}
/>
);
}
export function PhoneInputWithoutIconExample() {
const [phone, setPhone] = React.useState("");
return (
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
showIcon={false}
/>
);
}
export function PhoneInputWithBackgroundExample() {
const [phone, setPhone] = React.useState("");
return (
<div className="rounded-ele bg-muted/50 p-4">
<label className="mb-2 block text-sm font-medium">Phone Number</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
placeholder="Enter your phone number"
/>
</div>
);
}
export function PhoneInputErrorExample() {
const [phone, setPhone] = React.useState("");
return (
<div className="space-y-2">
<div className="rounded-ele bg-destructive/5 p-3">
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
className="bg-transparent"
/>
</div>
<p className="text-sm text-destructive">
Please enter a valid phone number
</p>
</div>
);
}
export function PhoneInputDisabledExample() {
return <PhoneInput value="+1 (555) 123-4567" disabled />;
}
export function PhoneInputFormExample() {
const [formData, setFormData] = React.useState({
primaryPhone: "",
secondaryPhone: "",
});
return (
<div className="max-w-md space-y-4">
<div>
<label className="mb-2 block text-sm font-medium">Primary Phone</label>
<PhoneInput
value={formData.primaryPhone}
onChange={(value, formatted) =>
setFormData((prev) => ({ ...prev, primaryPhone: formatted }))
}
defaultCountry="US"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium">
Secondary Phone (Optional)
</label>
<PhoneInput
value={formData.secondaryPhone}
onChange={(value, formatted) =>
setFormData((prev) => ({ ...prev, secondaryPhone: formatted }))
}
defaultCountry="GB"
placeholder="Alternative contact number"
/>
</div>
{(formData.primaryPhone || formData.secondaryPhone) && (
<div className="rounded-ele bg-muted p-3 text-sm">
<p className="font-medium">Contact Information:</p>
{formData.primaryPhone && <p>Primary: {formData.primaryPhone}</p>}
{formData.secondaryPhone && (
<p>Secondary: {formData.secondaryPhone}</p>
)}
</div>
)}
</div>
);
}
export function InternationalPhoneExample() {
const [contacts, setContacts] = React.useState([
{ id: 1, name: "John Doe", phone: "", country: "US" },
{ id: 2, name: "Maria Garcia", phone: "", country: "ES" },
{ id: 3, name: "Hiroshi Tanaka", phone: "", country: "JP" },
]);
const updateContact = (id: number, phone: string) => {
setContacts((prev) =>
prev.map((contact) =>
contact.id === id ? { ...contact, phone } : contact
)
);
};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold">International Contacts</h3>
{contacts.map((contact) => (
<div key={contact.id} className="space-y-2">
<label className="block text-sm font-medium">{contact.name}</label>
<PhoneInput
value={contact.phone}
onChange={(value, formatted) =>
updateContact(contact.id, formatted)
}
defaultCountry={contact.country}
placeholder={`Enter ${contact.name.split(" ")[0]}'s phone number`}
/>
</div>
))}
</div>
);
}
export function PhoneInputWithValidationExample() {
const [phone, setPhone] = React.useState("");
const [isValid, setIsValid] = React.useState(false);
return (
<div className="space-y-4">
<div>
<label className="mb-2 block text-sm font-medium">
Phone Number with Validation
</label>
<PhoneInput
value={phone}
onChange={(value, formatted, countryCode, valid) => {
setPhone(formatted);
setIsValid(valid || false);
}}
showValidation={true}
onValidationChange={setIsValid}
defaultCountry="US"
/>
</div>
<div className="text-sm">
<p className={isValid ? "text-green-600" : "text-gray-500"}>
Status:{" "}
{isValid ? "Valid phone number" : "Enter a valid phone number"}
</p>
{phone && <p className="text-gray-500">Phone: {phone}</p>}
</div>
</div>
);
}
export function CountrySpecificPlaceholdersExample() {
const [selectedCountries] = React.useState(["US", "GB", "DE", "JP", "BR"]);
const [phones, setPhones] = React.useState<Record<string, string>>({});
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold">Country-Specific Placeholders</h3>
<p className="text-sm text-muted-foreground">
Notice how the placeholder changes based on the selected country's phone
format.
</p>
{selectedCountries.map((countryCode) => {
const country = countries.find((c) => c.code === countryCode);
return (
<div key={countryCode} className="space-y-2">
<label className="block text-sm font-medium">
{country?.name} ({country?.phoneCode})
</label>
<PhoneInput
value={phones[countryCode] || ""}
onChange={(value, formatted) =>
setPhones((prev) => ({ ...prev, [countryCode]: formatted }))
}
defaultCountry={countryCode}
showValidation={true}
/>
<p className="text-xs text-muted-foreground">
Expected format: {country?.placeholder}
</p>
</div>
);
})}
</div>
);
}
export function PhoneInputValidationFormExample() {
const [formData, setFormData] = React.useState({
phone: "",
isPhoneValid: false,
});
const [submitted, setSubmitted] = React.useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
if (formData.isPhoneValid) {
alert("Form submitted successfully!");
setSubmitted(false);
}
};
return (
<form onSubmit={handleSubmit} className="max-w-md space-y-4">
<div>
<label className="mb-2 block text-sm font-medium">Phone Number *</label>
<PhoneInput
value={formData.phone}
onChange={(value, formatted, countryCode, isValid) => {
setFormData((prev) => ({
...prev,
phone: formatted,
isPhoneValid: isValid || false,
}));
}}
showValidation={true}
defaultCountry="US"
className={
submitted && !formData.isPhoneValid
? "border-red-200 bg-red-50"
: ""
}
/>
{submitted && !formData.isPhoneValid && (
<p className="mt-1 text-sm text-red-600">
Please enter a valid phone number.
</p>
)}
</div>
<button
type="submit"
className="w-full rounded-ele bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
disabled={!formData.phone}
>
Submit
</button>
</form>
);
}
export { phoneInputVariants };
npx hextaui@latest add phone-input
Usage
import { PhoneInput } from "@/components/ui/phone-input";
<PhoneInput
value={phoneNumber}
onChange={(value, formatted, countryCode) => {
setPhoneNumber(formatted);
}}
/>
Examples
Variants
function PhoneInputVariantsExample() {
const [phone, setPhone] = React.useState("");
return (
<div className="flex flex-col gap-4">
<div>
<label className="mb-2 block text-sm font-medium">Default</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
variant="default"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Outline</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
variant="outline"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Ghost</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
variant="ghost"
/>
</div>
</div>
);
}
Sizes
function PhoneInputSizesExample() {
const [phone, setPhone] = React.useState("");
return (
<div className="flex flex-col gap-4">
<div>
<label className="mb-2 block text-sm font-medium">Small</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
size="sm"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Default</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
size="default"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Large</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
size="lg"
/>
</div>
</div>
);
}
Without Flag
function PhoneInputWithoutFlagExample() {
const [phone, setPhone] = React.useState("");
return (
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
showFlag={false}
/>
);
}
Without Icon
function PhoneInputWithoutIconExample() {
const [phone, setPhone] = React.useState("");
return (
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
showIcon={false}
/>
);
}
With Background Container
Perfect for forms where you want to provide visual context or grouping.
function PhoneInputWithBackgroundExample() {
const [phone, setPhone] = React.useState("");
return (
<div className="rounded-lg bg-muted/50 p-4">
<label className="mb-2 block text-sm font-medium">Phone Number</label>
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
placeholder="Enter your phone number"
/>
</div>
);
}
Error State
Please enter a valid phone number
function PhoneInputErrorExample() {
const [phone, setPhone] = React.useState("");
return (
<div className="space-y-2">
<div className="rounded-md bg-destructive/5 p-3">
<PhoneInput
value={phone}
onChange={(value, formatted) => setPhone(formatted)}
className="bg-transparent"
/>
</div>
<p className="text-sm text-destructive">Please enter a valid phone number</p>
</div>
);
}
Disabled State
function PhoneInputDisabledExample() {
return (
<PhoneInput
value="+1 (555) 123-4567"
disabled
/>
);
}
Form Integration
Perfect for contact forms, registration, and profile management.
function PhoneInputFormExample() {
const [formData, setFormData] = React.useState({
primaryPhone: "",
secondaryPhone: "",
});
return (
<div className="max-w-md space-y-4">
<div>
<label className="mb-2 block text-sm font-medium">Primary Phone</label>
<PhoneInput
value={formData.primaryPhone}
onChange={(value, formatted) =>
setFormData((prev) => ({ ...prev, primaryPhone: formatted }))
}
defaultCountry="US"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Secondary Phone (Optional)</label>
<PhoneInput
value={formData.secondaryPhone}
onChange={(value, formatted) =>
setFormData((prev) => ({ ...prev, secondaryPhone: formatted }))
}
defaultCountry="GB"
placeholder="Alternative contact number"
/>
</div>
{(formData.primaryPhone || formData.secondaryPhone) && (
<div className="rounded-md bg-muted p-3 text-sm">
<p className="font-medium">Contact Information:</p>
{formData.primaryPhone && <p>Primary: {formData.primaryPhone}</p>}
{formData.secondaryPhone && <p>Secondary: {formData.secondaryPhone}</p>}
</div>
)}
</div>
);
}
International Contacts
Great for applications with global users and international contact management.
International Contacts
function InternationalPhoneExample() {
const [contacts, setContacts] = React.useState([
{ id: 1, name: "John Doe", phone: "", country: "US" },
{ id: 2, name: "Maria Garcia", phone: "", country: "ES" },
{ id: 3, name: "Hiroshi Tanaka", phone: "", country: "JP" },
]);
const updateContact = (id: number, phone: string) => {
setContacts((prev) =>
prev.map((contact) =>
contact.id === id ? { ...contact, phone } : contact
)
);
};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold">International Contacts</h3>
{contacts.map((contact) => (
<div key={contact.id} className="space-y-2">
<label className="block text-sm font-medium">{contact.name}</label>
<PhoneInput
value={contact.phone}
onChange={(value, formatted) => updateContact(contact.id, formatted)}
defaultCountry={contact.country}
placeholder={`Enter ${contact.name.split(' ')[0]}'s phone number`}
/>
</div>
))}
</div>
);
}
Phone Number Validation
Built-in validation with country-specific patterns and visual feedback indicators.
Status: Enter a valid phone number
function PhoneInputWithValidationExample() {
const [phone, setPhone] = React.useState("");
const [isValid, setIsValid] = React.useState(false);
return (
<div className="space-y-4">
<div>
<label className="mb-2 block text-sm font-medium">
Phone Number with Validation
</label>
<PhoneInput
value={phone}
onChange={(value, formatted, countryCode, valid) => {
setPhone(formatted);
setIsValid(valid || false);
}}
showValidation={true}
onValidationChange={setIsValid}
defaultCountry="US"
/>
</div>
<div className="text-sm">
<p className={isValid ? "text-green-600" : "text-gray-500"}>
Status: {isValid ? "Valid phone number" : "Enter a valid phone number"}
</p>
{phone && (
<p className="text-gray-500">
Phone: {phone}
</p>
)}
</div>
</div>
);
}
Country-Specific Placeholders
Automatic placeholder updates based on selected country's phone format.
Country-Specific Placeholders
Notice how the placeholder changes based on the selected country's phone format.
Expected format: (555) 123-4567
Expected format: 7911 123456
Expected format: 151 12345678
Expected format: 90 1234 5678
Expected format: (11) 91234-5678
function CountrySpecificPlaceholdersExample() {
const [selectedCountries] = React.useState(["US", "GB", "DE", "JP", "BR"]);
const [phones, setPhones] = React.useState<Record<string, string>>({});
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold">Country-Specific Placeholders</h3>
<p className="text-sm text-muted-foreground">
Notice how the placeholder changes based on the selected country's phone format.
</p>
{selectedCountries.map((countryCode) => {
const country = countries.find(c => c.code === countryCode);
return (
<div key={countryCode} className="space-y-2">
<label className="block text-sm font-medium">
{country?.name} ({country?.phoneCode})
</label>
<PhoneInput
value={phones[countryCode] || ""}
onChange={(value, formatted) =>
setPhones(prev => ({ ...prev, [countryCode]: formatted }))
}
defaultCountry={countryCode}
showValidation={true}
/>
<p className="text-xs text-muted-foreground">
Expected format: {country?.placeholder}
</p>
</div>
);
})}
</div>
);
}
Form with Validation
Complete form integration with validation and error handling.
function PhoneInputValidationFormExample() {
const [formData, setFormData] = React.useState({
phone: "",
isPhoneValid: false,
});
const [submitted, setSubmitted] = React.useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
if (formData.isPhoneValid) {
alert("Form submitted successfully!");
setSubmitted(false);
}
};
return (
<form onSubmit={handleSubmit} className="max-w-md space-y-4">
<div>
<label className="mb-2 block text-sm font-medium">
Phone Number *
</label>
<PhoneInput
value={formData.phone}
onChange={(value, formatted, countryCode, isValid) => {
setFormData(prev => ({
...prev,
phone: formatted,
isPhoneValid: isValid || false,
}));
}}
showValidation={true}
defaultCountry="US"
className={
submitted && !formData.isPhoneValid
? "border-red-200 bg-red-50"
: ""
}
/>
{submitted && !formData.isPhoneValid && (
<p className="mt-1 text-sm text-red-600">
Please enter a valid phone number.
</p>
)}
</div>
<button
type="submit"
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
disabled={!formData.phone}
>
Submit
</button>
</form>
);
}
API Reference
PhoneInput
Prop | Type | Default |
---|---|---|
value? | string | - |
onChange? | (value: string, formattedValue: string, countryCode: string, isValid?: boolean) => void | - |
placeholder? | string | "Enter phone number" |
disabled? | boolean | false |
defaultCountry? | string | "US" |
showFlag? | boolean | true |
showIcon? | boolean | true |
error? | boolean | false |
showValidation? | boolean | false |
onValidationChange? | (isValid: boolean) => void | - |
variant? | "default" | "outline" | "ghost" | "default" |
size? | "sm" | "default" | "lg" | "default" |
className? | string | - |
Features
- International Support: 50+ countries with flags and phone codes
- Smart Validation: Country-specific phone number validation with visual feedback
- Dynamic Placeholders: Auto-updating placeholders based on selected country format
- Borderless Design: Clean, spacious design without borders or focus rings
- Auto-formatting: Automatic phone code detection and formatting
- Input Constraints: Country-specific max length limits prevent invalid entries
- Responsive Design: Works seamlessly on all device sizes
- Accessible: Built with proper ARIA labels and keyboard navigation
- Flexible Styling: Multiple variants and customizable appearance
- Form Ready: Easy integration with forms and validation libraries
- TypeScript: Full TypeScript support with proper type definitions
- Performance: Optimized with React.memo and useCallback
Supported Countries
The component includes support for 50+ countries including:
- North America: United States, Canada, Mexico
- Europe: United Kingdom, Germany, France, Italy, Spain, Netherlands, and more
- Asia: Japan, South Korea, China, India, Singapore, Thailand, and more
- Middle East: United Arab Emirates, Saudi Arabia, Israel
- Africa: South Africa, Nigeria, Kenya, Ghana, Egypt
- South America: Brazil, Argentina, Chile, Colombia, Peru
Country Code Format
All country codes follow the ISO 3166-1 alpha-2 standard (e.g., "US", "GB", "DE").
Phone Code Detection
The component automatically detects and parses phone codes when a formatted phone number is provided as the initial value, making it easy to populate from existing data sources.
Last updated on