Components

Country Input

A searchable country combobox with emoji flags and a built-in ISO list with dial codes.

input

Installation

npx cano-ui add country-input

Installs button, command, popover, lucide-react automatically if missing.

Source

"use client"

import * as React from "react"
import { Check, ChevronsUpDown } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

export interface Country {
  /** ISO 3166-1 alpha-2 code, e.g. "US". */
  code: string
  name: string
  /** Calling code without the +, e.g. "44". */
  dialCode: string
}

/** ISO code → flag emoji via regional indicator symbols. */
export function countryFlag(code: string) {
  return String.fromCodePoint(
    ...[...code.toUpperCase()].map((c) => 0x1f1e6 + c.charCodeAt(0) - 65)
  )
}

const data: [string, string, string][] = [
  ["US", "United States", "1"], ["GB", "United Kingdom", "44"],
  ["CA", "Canada", "1"], ["AU", "Australia", "61"], ["DE", "Germany", "49"],
  ["FR", "France", "33"], ["ES", "Spain", "34"], ["IT", "Italy", "39"],
  ["NL", "Netherlands", "31"], ["BE", "Belgium", "32"],
  ["CH", "Switzerland", "41"], ["AT", "Austria", "43"], ["SE", "Sweden", "46"],
  ["NO", "Norway", "47"], ["DK", "Denmark", "45"], ["FI", "Finland", "358"],
  ["IE", "Ireland", "353"], ["PT", "Portugal", "351"], ["GR", "Greece", "30"],
  ["PL", "Poland", "48"], ["CZ", "Czechia", "420"], ["HU", "Hungary", "36"],
  ["RO", "Romania", "40"], ["BG", "Bulgaria", "359"], ["HR", "Croatia", "385"],
  ["SK", "Slovakia", "421"], ["SI", "Slovenia", "386"],
  ["UA", "Ukraine", "380"], ["RU", "Russia", "7"], ["TR", "Türkiye", "90"],
  ["IS", "Iceland", "354"], ["LU", "Luxembourg", "352"], ["MT", "Malta", "356"],
  ["CY", "Cyprus", "357"], ["EE", "Estonia", "372"], ["LV", "Latvia", "371"],
  ["LT", "Lithuania", "370"], ["RS", "Serbia", "381"],
  ["BA", "Bosnia and Herzegovina", "387"], ["AL", "Albania", "355"],
  ["MK", "North Macedonia", "389"], ["GE", "Georgia", "995"],
  ["AM", "Armenia", "374"], ["AZ", "Azerbaijan", "994"],
  ["KZ", "Kazakhstan", "7"], ["UZ", "Uzbekistan", "998"],
  ["IL", "Israel", "972"], ["SA", "Saudi Arabia", "966"],
  ["AE", "United Arab Emirates", "971"], ["QA", "Qatar", "974"],
  ["KW", "Kuwait", "965"], ["BH", "Bahrain", "973"], ["OM", "Oman", "968"],
  ["JO", "Jordan", "962"], ["LB", "Lebanon", "961"], ["IQ", "Iraq", "964"],
  ["IR", "Iran", "98"], ["AF", "Afghanistan", "93"], ["EG", "Egypt", "20"],
  ["MA", "Morocco", "212"], ["DZ", "Algeria", "213"], ["TN", "Tunisia", "216"],
  ["NG", "Nigeria", "234"], ["GH", "Ghana", "233"], ["KE", "Kenya", "254"],
  ["ET", "Ethiopia", "251"], ["TZ", "Tanzania", "255"], ["UG", "Uganda", "256"],
  ["ZA", "South Africa", "27"], ["IN", "India", "91"],
  ["PK", "Pakistan", "92"], ["BD", "Bangladesh", "880"],
  ["LK", "Sri Lanka", "94"], ["NP", "Nepal", "977"], ["CN", "China", "86"],
  ["JP", "Japan", "81"], ["KR", "South Korea", "82"], ["TW", "Taiwan", "886"],
  ["HK", "Hong Kong", "852"], ["SG", "Singapore", "65"],
  ["MY", "Malaysia", "60"], ["TH", "Thailand", "66"], ["VN", "Vietnam", "84"],
  ["PH", "Philippines", "63"], ["ID", "Indonesia", "62"],
  ["KH", "Cambodia", "855"], ["MM", "Myanmar", "95"],
  ["NZ", "New Zealand", "64"], ["FJ", "Fiji", "679"], ["BR", "Brazil", "55"],
  ["AR", "Argentina", "54"], ["CL", "Chile", "56"], ["CO", "Colombia", "57"],
  ["PE", "Peru", "51"], ["VE", "Venezuela", "58"], ["EC", "Ecuador", "593"],
  ["UY", "Uruguay", "598"], ["PY", "Paraguay", "595"],
  ["BO", "Bolivia", "591"], ["MX", "Mexico", "52"],
  ["GT", "Guatemala", "502"], ["CR", "Costa Rica", "506"],
  ["PA", "Panama", "507"], ["DO", "Dominican Republic", "1"],
  ["JM", "Jamaica", "1"], ["CU", "Cuba", "53"],
]

export const countries: Country[] = data.map(([code, name, dialCode]) => ({
  code,
  name,
  dialCode,
}))

export interface CountryInputProps {
  /** ISO code of the selected country. */
  value?: string
  onValueChange?: (country: Country) => void
  placeholder?: string
  /** Override or narrow the built-in list. */
  options?: Country[]
  disabled?: boolean
  className?: string
}

export function CountryInput({
  value,
  onValueChange,
  placeholder = "Select country…",
  options = countries,
  disabled,
  className,
}: CountryInputProps) {
  const [open, setOpen] = React.useState(false)
  const selected = options.find((c) => c.code === value)

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          aria-label={selected ? selected.name : placeholder}
          disabled={disabled}
          data-slot="country-input"
          className={cn(
            "w-full justify-between font-normal",
            !selected && "text-muted-foreground",
            className
          )}
        >
          {selected ? (
            <span className="flex min-w-0 items-center gap-2">
              <span aria-hidden="true">{countryFlag(selected.code)}</span>
              <span className="truncate">{selected.name}</span>
            </span>
          ) : (
            placeholder
          )}
          <ChevronsUpDown
            aria-hidden="true"
            className="text-muted-foreground"
          />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-(--radix-popover-trigger-width) p-0">
        <Command>
          <CommandInput placeholder="Search countries…" />
          <CommandList>
            <CommandEmpty>No country found.</CommandEmpty>
            <CommandGroup>
              {options.map((country) => (
                <CommandItem
                  key={country.code}
                  value={country.name}
                  onSelect={() => {
                    onValueChange?.(country)
                    setOpen(false)
                  }}
                >
                  <span aria-hidden="true">{countryFlag(country.code)}</span>
                  <span className="flex-1 truncate">{country.name}</span>
                  <Check
                    aria-hidden="true"
                    className={cn(
                      country.code === value ? "opacity-100" : "opacity-0"
                    )}
                  />
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  )
}

This exact file lands in your project at components/ui/country-input.tsx. It is yours to edit.