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

Region Select

A searchable combobox of states/provinces for a country — Combobox-powered filtering, with a free-text fallback for countries with no subdivisions.

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

Last updated

Install

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

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

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

Usage

import { RegionSelect } from "@/components/ui/region-select";

const [state, setState] = React.useState("");

<RegionSelect country="US" value={state} onValueChange={setState} />;

RegionSelect is controlled: pass the selected subdivision code as value and update it from onValueChange. The country prop (an ISO-3166-1 alpha-2 code) decides which subdivisions are offered — and whether the searchable dropdown or the free-text fallback is shown.

It is a thin, data-fed wrapper over SearchableSelect for the combobox path, or Input for the fallback path. A leading MapPin icon marks the field as a location input in both modes. The dataset itself is the separate geo-data item, so installing this component alongside CountrySelect copies the ISO data once.

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

Examples

Countries

The REGIONS dataset covers 45 countries and 1,187 subdivisions: US states, Canadian provinces, Australian states/territories, Indian states/UTs, UK regions, and high-volume billing/shipping markets across the Americas, Europe, Africa, the Middle East, and Asia-Pacific. Each supported country renders a searchable combobox with live filtering; countries outside the dataset fall back to free text.

States

A country with no predefined subdivisions (e.g. Singapore) falls back to a plain text Input so the value is still captured — and disabled makes the control inert.

Clearing

While a state is selected, the trigger carries an explicit clear control; activating it fires onValueChange("") and returns the trigger to its placeholder. Pass clearable={false} to remove it.

It replaces the old re-select-to-clear toggle, which was undiscoverable and — more importantly — computed the value inside each row's click handler while the Combobox root's onValueChange stayed unwired, so a pointer click and keyboard Enter reached the value by two different code paths. Selection now runs through one path for both.

value: "CA"

Empty results

When a search query matches no subdivision, the listbox shows a "No state found." message. The selection is unchanged until the user picks a matching row.

API Reference

RegionSelect

PropTypeDefaultDescription
country*stringISO-3166-1 alpha-2 country code that determines the available states (e.g. "US", "CA").
aria-labelstringAccessible name for the trigger / input. role="combobox" prohibits name-from-content, so the control always needs an explicit label; defaults to the selected state's name, falling back to the placeholder.
classNamestringAdditional className for the trigger / input element.
clearablebooleantrueShows a clear control on the trigger while a state is selected — the explicit replacement for the old click-again-to-clear toggle. Has no effect on the free-text fallback.
containerClassNamestringAdditional className for the outer root wrapper.
disabledbooleanfalseDisable the control entirely.
idstringid forwarded to the trigger / input for label association.
onValueChange((value: string) => void)Called with the new state code when the selection changes (or the free-text value for fallback countries).
placeholderstring"Select state"Placeholder shown in the trigger / input when nothing is selected.
refReact.Ref<HTMLDivElement>Ref forwarded to the component's root <div> (wraps either the combobox or the fallback input).
valuestring""The currently selected state code (controlled). Empty string when nothing is selected.

Data attributes and CSS variables on RegionSelect

AttributeValues
data-fallback""
data-slot"region-select"

className is applied to the focusable trigger/input. Use containerClassName for wrapper layout classes.

Data API

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

import { getRegions, REGIONS, type Region } from "@/lib/geo-data";

getRegions("us").length; // 50+ (case-insensitive)
getRegions("SG"); // [] — no predefined subdivisions; use the free-text fallback

getRegions("CA") → 13 subdivisions

Region

PropTypeDefaultDescription
code*stringThe subdivision code, stored as the selected value (e.g. "CA").
name*stringThe human-readable subdivision name shown in the list (e.g. "California").
PropTypeDefaultDescription
REGIONSRecord<string, Region[]>Subdivisions keyed by ISO-3166-1 alpha-2 country code (45 countries, 1,187 subdivisions). A country absent from the map has no predefined states and falls back to free text.
getRegions(country: string) => Region[]The states/provinces for a country code (case-insensitive). Returns an empty array when the country has no predefined subdivisions — which is what RegionSelect reads to choose the free-text fallback.

Accessibility

  • The trigger exposes role="combobox" with aria-expanded/aria-haspopup="dialog"; opening reveals a listbox of options with live, type-to-filter search inside the popup.
  • Full keyboard support is handled by the underlying Combobox: type to filter, / to move the highlight, Enter to select, and Esc to dismiss — focus returns to the trigger on close.
  • The free-text fallback is a native <input>; associate a label via id for both modes.
  • Focus re-colors the trigger/search border with the ring token (border-ring/(--alpha-tint-border)) — never outline: none.
KeyAction
Enter / SpaceOpen the popover from the trigger.
/ Move the highlight between matching states.
EnterSelect the highlighted state and close.
EscClose without changing the selection.
ContractStates tested
Behaviourdefault, disabled, empty, filtering, open, selected
Accessibilitydisabled, labeled
Visualdefault, empty

Do / Don't

Do
Store the subdivision code (e.g. 'CA') as the value, and pass the country so the right states — or the free-text fallback — are shown.
Don't
Hardcode a single country's state list inline — pass country and let RegionSelect pick the dataset or fall back to a text input.

On this page