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
- 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-selectThe 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
| Prop | Type | Default | Description |
|---|---|---|---|
country* | string | — | ISO-3166-1 alpha-2 country code that determines the available states (e.g. "US", "CA"). |
aria-label | string | — | Accessible 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. |
className | string | — | Additional className for the trigger / input element. |
clearable | boolean | true | Shows 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. |
containerClassName | string | — | Additional className for the outer root wrapper. |
disabled | boolean | false | Disable the control entirely. |
id | string | — | id 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). |
placeholder | string | "Select state" | Placeholder shown in the trigger / input when nothing is selected. |
ref | React.Ref<HTMLDivElement> | — | Ref forwarded to the component's root <div> (wraps either the combobox or the fallback input). |
value | string | "" | The currently selected state code (controlled). Empty string when nothing is selected. |
Data attributes and CSS variables on RegionSelect
| Attribute | Values |
|---|---|
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 fallbackgetRegions("CA") → 13 subdivisions
Region
| Prop | Type | Default | Description |
|---|---|---|---|
code* | string | — | The subdivision code, stored as the selected value (e.g. "CA"). |
name* | string | — | The human-readable subdivision name shown in the list (e.g. "California"). |
| Prop | Type | Default | Description |
|---|---|---|---|
REGIONS | Record<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"witharia-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 viaidfor both modes. - Focus re-colors the trigger/search border with the
ringtoken (border-ring/(--alpha-tint-border)) — neveroutline: none.
| Key | Action |
|---|---|
| Enter / Space | Open the popover from the trigger. |
| ↑ / ↓ | Move the highlight between matching states. |
| Enter | Select the highlighted state and close. |
| Esc | Close without changing the selection. |
| Contract | States tested |
|---|---|
| Behaviour | default, disabled, empty, filtering, open, selected |
| Accessibility | disabled, labeled |
| Visual | default, empty |