Phone number inputs with country codes are one of those deceptively tricky UI components. Here's how to build a polished one with flag images and dial codes using ApogeoAPI.
What We're Building
A phone number input that:
- Shows a flag + country dial code in a dropdown
- Lets the user search countries by name
- Combines the dial code with the number the user types
- Is fully TypeScript-typed
Step 1: Fetch Countries with Phone Codes
The ApogeoAPI countries endpoint returns phone_code and flag_url for every country.
// types.ts
export interface Country {
iso2: string;
name: string;
phone_code: string;
flag_url: string;
}
// useCountries.ts
import { useEffect, useState } from 'react';
export function useCountries() {
const [countries, setCountries] = useState([]);
useEffect(() => {
fetch('https://api.apogeoapi.com/v1/countries', {
headers: { 'X-API-Key': process.env.NEXT_PUBLIC_APOGEO_KEY! },
})
.then(r => r.json())
.then(setCountries);
}, []);
return countries;
}
Step 2: Build the PhoneInput Component
'use client';
import { useState, useMemo } from 'react';
import { useCountries } from './useCountries';
interface Props {
value: string;
onChange: (fullNumber: string) => void;
}
export function PhoneInput({ value, onChange }: Props) {
const countries = useCountries();
const [selectedIso, setSelectedIso] = useState('US');
const [number, setNumber] = useState('');
const [search, setSearch] = useState('');
const [open, setOpen] = useState(false);
const selected = countries.find(c => c.iso2 === selectedIso);
const filtered = useMemo(() =>
countries.filter(c =>
c.name.toLowerCase().includes(search.toLowerCase())
), [countries, search]);
const handleSelect = (country: Country) => {
setSelectedIso(country.iso2);
setOpen(false);
setSearch('');
onChange(`+${country.phone_code}${number}`);
};
const handleNumber = (e: React.ChangeEvent) => {
setNumber(e.target.value);
onChange(`+${selected?.phone_code ?? ''}${e.target.value}`);
};
return (
{/* Dial code selector */}
setOpen(!open)}
className="flex items-center gap-1 border rounded px-3 py-2 min-w-[90px]">
{selected && (
<>
+{selected.phone_code}
)}
{/* Dropdown */}
{open && (
setSearch(e.target.value)}
className="w-full px-3 py-2 border-b text-sm outline-none" />
{filtered.map(c => (
handleSelect(c)}
className="flex items-center gap-2 px-3 py-2 hover:bg-gray-50 cursor-pointer text-sm">
{c.name}
+{c.phone_code}
))}
)}
{/* Number input */}
);
}
Step 3: Handle the Combined Value
The onChange callback fires with the full international number: +1 4155552671. Store it in your form state and send it as-is to your backend.
Optimizations
- Cache the country list: Countries change rarely. Use SWR with a 24-hour stale time or cache in localStorage.
-
Default to user's country: Combine with the IP geolocation endpoint —
GET /v1/geolocate/auto— to pre-select the right dial code automatically. -
Memoize filtered list: The
useMemoabove ensures filtering is fast even with 250 countries.
Originally published at https://apogeoapi.com/blog/phone-flag-selector-react. Try ApogeoAPI free at apogeoapi.com.
Top comments (0)