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
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Region Select before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify region-selectInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/region-selectRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/geo-data, @vegastack/input-group, @vegastack/searchable-select.
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 an
InputGroup for the free-text 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 free-text
InputGroup 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 an
InputGroupwith a leading pin addon and anInputGroupInput; associate a label viaidfor both modes. - The trigger is a
Button, so it takes the global:focus-visiblebackground tint. The in-panel search row and the free-text fallback are text entry: while their input holds focus their border darkens subtly (border-ring/50, easing in over 150ms) and nothing fills — FOC-14's text-entry exception (MK 2026-09-27). Neither path setsoutline: nonewithout a replacement.
| 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 |