Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Country Select

A button-triggered searchable country popover returning the ISO 3166-1 alpha-2 code.

Status
stable
Since
0.1.0
Accessibility pattern
APG combobox (dialog popup)

Last updated

Install

Add Country Select from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/country-select

The same command installs the registry items it composes: @vegastack/searchable-select, @vegastack/geo-data.

Usage

import { CountrySelect } from "@/components/ui/country-select";

const [country, setCountry] = useState<string>();
<CountrySelect value={country} onValueChange={setCountry} />;

CountrySelect is controlled: pass the selected country's ISO 3166-1 alpha-2 code as value and update it from onValueChange. The component is a single composite — there are no subcomponents to compose.

Examples

Anatomy

CountrySelect is a thin, data-fed wrapper over SearchableSelect: the dataset goes in, the ISO code comes out, and every structural decision — the Select-shaped trigger, the search field inside the panel, the check on the selected row, the clear control — belongs to that preset.

<CountrySelect value={code} onValueChange={setCode} />
// ↓ renders, internally:
//   <SearchableSelect
//     items={countries}                    ← the geo-data COUNTRIES array by default
//     value={selected} onValueChange={…}   ← the ONE selection path
//     renderItem={…}                       ← flag + name per row
//     searchLabel="Search countries"       ← the in-panel search field
//   />

The trigger is w-full, like every other form control — constrain it with a parent (<div className="max-w-xs">), not with a width of its own; the panel matches the trigger's width.

Country selection

A flag + name list with type-to-filter search inside a popup; the value is the ISO 3166-1 alpha-2 code.

Selected & disabled

With a value set, the trigger shows the country's flag + name and the matching row carries a check; disabled renders an inert trigger that won't open.

Empty

Nothing selected yet, so the trigger shows a muted placeholder. Open it and type to filter — when no country matches the search, the list shows the empty-results message "No country found."

Custom dataset

Pass a countries array to restrict or extend the offered list (it defaults to the full COUNTRIES set from geo-data). The selected value is still the ISO code.

API Reference

PropTypeDefaultDescription
aria-labelstringAccessible name for the trigger. Defaults to the selected country name or the placeholder.
classNamestringAdditional class names merged onto the trigger button.
clearablebooleanfalseShows a clear control on the trigger while a country is selected.
countriesCountry[]COUNTRIESOverride the country list.
disabledbooleanfalseDisable the control.
idstringid forwarded to the trigger for label association.
onValueChange((code: string) => void)Fired with the selected ISO code when the user picks a country, or "" when it is cleared.
placeholderstring'Select country'Placeholder shown on the trigger when nothing is selected.
refReact.Ref<HTMLButtonElement>Ref forwarded to the trigger button — the component's focusable root (the popover panel is portaled, so the trigger is the stable host element to focus/measure).
valuestringThe selected country's ISO 3166-1 alpha-2 code (controlled).

Data attributes and CSS variables on CountrySelect

AttributeValues
data-slot"country-select"

Data API

The dataset is its own registry item, geo-data, installed automatically as a dependency of this component and shared with RegionSelect — install both and the ISO data lands in your tree once. Import from your lib alias, not from the component.

Country

PropTypeDefaultDescription
code*stringISO 3166-1 alpha-2 code, stored as the selected value (e.g. "US").
flag*stringThe flag emoji (regional indicator pair).
name*stringThe human-readable country name shown in the list (e.g. "United States").

COUNTRIES

PropTypeDefaultDescription
COUNTRIESCountry[]The full dataset — 198 ISO 3166-1 alpha-2 entries (code + English name + flag emoji), covering all regions. The default value of the countries prop. Extend it by editing the array; codes are unique.

getCountryByCode(code)

PropTypeDefaultDescription
getCountryByCode(code: string | undefined) => Country | undefinedCase-insensitive lookup of a country by its ISO 3166-1 alpha-2 code. Returns undefined for an unknown or missing code.
import { getCountryByCode } from "@/lib/geo-data";

getCountryByCode("us")?.name; // "United States"  (case-insensitive)
getCountryByCode("ZZ"); // undefined

Accessibility

  • The trigger carries role="combobox", an explicit aria-label (the selected country name, or the placeholder), and aria-expanded/aria-haspopup="dialog" (Base UI's Select-style combobox pattern — the search input lives inside the popup).
  • The list is keyboard-navigable (↑/↓/Enter); typing filters by country name or code.
  • Flag emoji are aria-hidden (decorative); the name carries the accessible text.
  • With clearable, the clear control is a sibling of the trigger — never a child — so the trigger never becomes an interactive control containing another one.
KeyAction
Enter / SpaceOpen the popover from the trigger.
/ Move the highlight between matching countries.
EnterSelect the highlighted country and close.
EscClose without changing the selection.
ContractStates tested
Behaviourdefault, disabled, selected
Accessibilitydisabled, labeled
Visualdefault

Do / Don't

Do
Store the ISO 3166-1 alpha-2 code (US, GB, IN) as the value — stable and locale-independent.
Don't
Store the localized display name — it changes with language and breaks lookups.

On this page