All blocks
Country Picker
Searchable country picker with flags, Intl-localized names, and ISO 3166-1 values — built on the accessible combobox pattern.
Built withCombobox
Country
Install
1npx stylexblocks add country-pickerSource
components/blocks/base-ui/country-picker.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162import * as stylex from "@stylexjs/stylex";import { useMemo } from "react";import { Combobox, type ComboboxVariantProps } from "#components/ui/base-ui/combobox.tsx";import type { StyleOverrideProps } from "#lib/recipe-props.ts";
/** * ISO 3166-1 alpha-2 codes (plus XK for Kosovo, which most products need). * Display names and sort order come from the platform via Intl, so the list * localizes itself and never goes stale. */export const countryCodes = [ "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "XK", "YE", "YT", "ZA", "ZM", "ZW",];
export interface Country { /** ISO 3166-1 alpha-2 code — what your backend receives. */ value: string; /** Localized display name from Intl.DisplayNames. */ label: string;}
/** Localized, collator-sorted country list; `locale` defaults to the runtime's. */export function getCountries(locale?: string): Country[] { const displayNames = new Intl.DisplayNames(locale ? [locale] : undefined, { type: "region" }); const collator = new Intl.Collator(locale); return countryCodes .map((value) => ({ value, label: displayNames.of(value) ?? value })) .sort((a, b) => collator.compare(a.label, b.label));}
/** "NL" → 🇳🇱 via regional indicator symbols; falls back to letters on Windows. */function flagEmoji(code: string): string { return String.fromCodePoint(...[...code].map((c) => 0x1f1a5 + c.charCodeAt(0)));}
const styles = stylex.create({ flag: { flexShrink: 0, marginInlineEnd: "0.5rem", },});
export interface CountryPickerProps extends ComboboxVariantProps, StyleOverrideProps { label?: string; /** Trigger text while nothing is selected. */ placeholder?: string; searchPlaceholder?: string; /** ISO 3166-1 alpha-2 code of the initial selection. */ defaultValue?: string; /** ISO 3166-1 alpha-2 code of the controlled selection. */ value?: string; onValueChange?: (code: string) => void; /** Posts the selected code with native form submits. */ name?: string; /** Display-name locale; defaults to the runtime's locale. */ locale?: string; /** Override the offered countries, e.g. only shipping destinations. */ items?: Country[]; disabled?: boolean;}
/** * CountryPicker — country picker with a select-style trigger and a search * input inside the popup (Base UI's input-inside-popup combobox pattern). * The trigger is the visible form control; opening moves focus to the input, * which carries the ARIA combobox wiring, so filtering stays screen-reader- * accessible. Speaks ISO 3166-1 alpha-2 on the wire; names and sort order * localize through Intl. */export function CountryPicker({ label = "Country", placeholder = "Select country", searchPlaceholder = "Search countries…", defaultValue, value, onValueChange, name, locale, items, disabled, variant = "outline", size = "md", palette, style,}: CountryPickerProps) { const countries = useMemo(() => items ?? getCountries(locale), [items, locale]); const byCode = (code?: string) => countries.find((c) => c.value === code) ?? null;
return ( <Combobox.Root items={countries} itemToStringLabel={(country: Country | null) => country?.label ?? ""} itemToStringValue={(country: Country | null) => country?.value ?? ""} // match the localized name loosely, or the ISO code exactly filter={(country: Country, query: string) => country.label.toLowerCase().includes(query.toLowerCase()) || country.value.toLowerCase() === query.toLowerCase() } defaultValue={byCode(defaultValue) ?? undefined} value={value !== undefined ? (byCode(value) ?? undefined) : undefined} onValueChange={(country: Country | null) => onValueChange?.(country?.value ?? "")} name={name} disabled={disabled} variant={variant} size={size} palette={palette} style={style} > <Combobox.Label>{label}</Combobox.Label> <Combobox.Control> <Combobox.FieldTrigger> <Combobox.FieldValue placeholder={placeholder}> {(country: Country | null) => country ? ( <> <span {...stylex.props(styles.flag)} aria-hidden="true"> {flagEmoji(country.value)} </span> {country.label} </> ) : null } </Combobox.FieldValue> </Combobox.FieldTrigger> </Combobox.Control> <Combobox.Positioner> <Combobox.Panel> <Combobox.Search placeholder={searchPlaceholder} /> <Combobox.PanelList> <Combobox.Empty>No countries found</Combobox.Empty> <Combobox.List> {(country: Country) => ( <Combobox.Item key={country.value} value={country}> <span {...stylex.props(styles.flag)} aria-hidden="true"> {flagEmoji(country.value)} </span> <Combobox.ItemText>{country.label}</Combobox.ItemText> <Combobox.ItemIndicator /> </Combobox.Item> )} </Combobox.List> </Combobox.PanelList> </Combobox.Panel> </Combobox.Positioner> </Combobox.Root> );}