Build websites 10x faster with HextaUI Blocks — Learn more
UIUI

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.

components/ui/phone-input.tsx
"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

PropTypeDefault
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.

Edit on GitHub

Last updated on