CountrySelect provides a complete country picker with a labeled trigger, decorative flags, search, and a no-country choice. CountrySelect.Root, CountrySelect.Trigger, and CountrySelect.Options expose the same implementation for composition with Dropdown. Your application supplies country data and translated text and decides how selected identifiers map to stored values.
Usage
Supply prepared choices with unique stable string identifiers and display labels. The picker reports identifiers unchanged, independently of the displayed language. Flags are optional decorative React nodes. A choice can setdisabled: true to remain visible without allowing selection.
defaultValue initializes uncontrolled selection. Use value and onValueChange for controlled selection. Updating value externally updates both the trigger and the selected option, including while the popup is open.
Shared parts
Root owns the prepared choices and selected identifier. It adds no DOM element. Trigger reads that selection and displays its label and flag. Options reads the same data and renders searchable rows inside Dropdown.SelectionGroup. Dropdown owns opening, popup placement, pages, navigation, and focus.
closeOnSelect defaults to true. Set it to false on the shorthand or Options when selection should keep the popup open.
Clearing and cancellation
null represents no country. Selecting the no-country row reports null; passing value={null} clears a controlled picker. An empty string ('') is a valid supplied choice identifier and stays distinct from null.
onValueChange receives the next identifier or null and Dropdown selection event details: event, reason, trigger, cancel, and isCanceled. Call details.cancel() before updating controlled state to preserve the current selection and keep the popup open. Repeated selection of the current identifier does not emit another value change.
An identifier absent from countries displays noCountryLabel and the no-country icon. Rendering it does not emit a change or overwrite the host value. The application decides how to handle stored values absent from the current choices.
Search and interaction
Search matches prepared display labels. It ignores letter case, optional accents in Latin, Greek, Arabic, and Hebrew text, and Cyrillic stress marks, treats the Cyrillicё as е, and folds letters such as ø, æ, and ß into o, ae, and ss. It also ignores standalone accent characters such as ^, `, ´, ¨, and ˇ, so a pending dead key does not hide matches. Marks that form distinct letters remain significant, such as Japanese voicing marks, Indic vowel signs, and the Cyrillic й. A partly typed Korean syllable, such as ㅎ or 하, matches syllables that start with it, such as 한. noCountryLabel participates in the same search. A query with no matching options displays noResultsLabel.
Arrow keys, Home, and End move focus without changing the selected identifier. Enter or Space on an enabled option commits selection through its selection group. Escape and outside interaction use Dropdown dismissal and focus handling. Each picker has independent selection and popup state.
Dropdown search activation and IME protection still need integration work, as do lazy page sessions and inactive-page behavior. Both picker forms reset their own search on reopen, including popups kept mounted. Their shared implementation does not yet complete Dropdown’s page and browsing lifecycle.
disabled on Root or the shorthand prevents opening and selection. A picker with no supplied countries is disabled as well. If either condition occurs while its popup is open, the picker requests closure. Closing returns focus to the trigger. Controlled hosts must accept that close request through onOpenChange; the shorthand also suppresses the popup while disabled. After accepting the close request, enabling the picker again does not reopen it automatically. The trigger stays focusable and exposes aria-disabled. Disabled triggers skip consumer activation handlers. Function forms of className, style, and render receive disabled: true in the trigger state.
Labels and customization
Provide a visiblelabel, or use aria-label or aria-labelledby on the trigger. Referenced labels take precedence over a nonempty aria-label, which takes precedence over the visible label. With one of these names, the selected country describes the trigger. Otherwise the selected country names it. The popup inherits the trigger name unless caller-owned popup labels or a Dropdown title name it. The shorthand also names its options listbox from those trigger labels unless optionsProps supplies its own accessible name. In composed usage, give Options an aria-label or aria-labelledby for an explicit listbox name. Otherwise it references the registered Dropdown title or trigger content. A tooltip reveals the full selected label when it is truncated.
searchLabel supplies the search input’s placeholder and accessible name, noCountryLabel the no-country text, and noResultsLabel the empty-result text. They default to Search, No country, and No results. Pass translated strings when your application is localized. Custom Trigger children replace the selected country display; the caller owns that content and its accessible presentation.
Native trigger attributes, event handlers, className, style, ref, and render target the trigger button. wrapperProps targets the surrounding div that also contains the visible label. Options native props, ref, and render target its selection group div. Its render function receives { value, multiple: false }, where value is the public country identifier or null, rather than an internal option identifier. searchProps targets the search input; search value and change handling remain owned by the picker. The shorthand accepts the same search props directly and group attributes through optionsProps. Forward supplied props and refs when replacing an element through render.
Host integration and storage
The shorthand accepts Dropdown’sopen, defaultOpen, onOpenChange, onEscapeKeyDown, onInteractOutside, and defaultPage. Composed usage sets them on Dropdown.Root. onOpenChange currently reports only a boolean, matching Dropdown’s public contract. Opening event details and general opening cancellation remain unavailable upstream. Escape and outside dismissal callbacks can call event.preventDefault() to keep the popup open.
popupProps accepts all Dropdown.Content props except children, including popup native attributes, ref, render, positioning, portal destination, accessible names, and keepMounted. In composed usage, pass them directly to Dropdown.Content.
Keep dropdown IDs, focus stacks, field metadata, persistence, and editing lifecycle in the host adapter. Twenty’s frontend adapter uses stable country codes for picker identifiers and converts explicit selections back to existing source country-name values. Its stored empty string maps to picker null; unmatched legacy values stay stored until an explicit selection changes them. Translated display labels do not become stored country names. Applications with another storage format should make the conversion in their adapter as well.
Native form submission, reset, required, readOnly, and Field integration for this custom picker remain deferred.
Front component renderer
React and Preact front component fixtures verify pointer opening, filtering, and selection through the shorthand and compound APIs, including controlled and uncontrolled values, null clearing and reselection, canceled changes, external controlled updates, disabled interaction, translated labels, and decorative flags. They also verify native trigger and search input props and refs, plus trigger and option-group render targets and refs. Search keyboard activation and IME handling, page-session lifetime, and full renderer portal, geometry, scrolling, outside dismissal, and focus restoration acceptance remain open.Migration
Replace the previous empty-string no-country sentinel withnull in controlled values and selection state. An empty string ('') is now a valid choice identifier, so value="" no longer means no country. Keep each CountryChoice.value stable; existing string identifiers can remain unchanged and display labels can be translated independently.
Update onValueChange handlers to accept string | null. The callback now also receives eventDetails as its second argument, including cancel() to reject a selection. Handlers that do not need event details can continue using only the first argument.
If your storage uses '' for no country, convert it to null before passing it to the picker and convert reported null back to '' when persisting. Keep that compatibility conversion in the application adapter, and omit it when '' is an intentional choice identifier.
Props
ReactNode
Custom trigger content. Replaces the selected country presentation while retaining shared trigger behavior.
string | ((state: PopoverTriggerState) => string | undefined)
Class applied to the trigger.
number
default:"0"
How long to wait before closing the popover that was opened on hover.
Specified in milliseconds.Requires the
openOnHover prop.boolean
Closes the popup after a selection. Defaults to true. Cancelled selection keeps it open.
readonly CountryChoice[]
required
Prepared country choices with unique stable string values, caller-owned labels, and optional decorative flags and disabled states. An empty string is a valid choice value.
boolean
Whether the uncontrolled popup is initially open.
string
Dropdown page shown when the popup opens. Defaults to root.
string | null
Initial uncontrolled selected identifier. Defaults to null.
number
default:"300"
How long to wait before the popover may be opened on hover. Specified in milliseconds.Requires the
openOnHover prop.boolean
Prevents opening and selection while the trigger stays focusable. An empty country list is also disabled.
string
ID of the trigger. Generated when omitted.
string
Optional visible label that names the trigger.
boolean
default:"true"
Whether the component renders a native
<button> element when replacing it
via the render prop.
Set to false if the rendered element is not a button (e.g. <div>).string
Text of the no-country choice, also displayed when no country is selected.
string
Text displayed when no choice matches the search.
((event: DropdownDismissEvent) => void)
Called before Escape dismisses the popup. Call event.preventDefault() to keep it open.
((event: DropdownDismissEvent) => void)
Called before outside interaction or Tab dismisses the popup. Call event.preventDefault() to keep it open.
((open: boolean) => void)
Reports popup opening and closing with the next boolean state, matching Dropdown.Root.
((value: string | null, eventDetails: DropdownSelectionGroupChangeEventDetails) => void)
Reports the selected identifier or null with cancellable event details. Call details.cancel() to preserve selection and keep the popup open.
boolean
Controls whether the country popup is open.
boolean
default:"false"
Whether the popover should also open when the trigger is hovered.
Omit<CountrySelectOptionsProps, "closeOnSelect" | "searchLabel" | "noResultsLabel" | "searchProps">
Native props, ref, and render for the selection group div rendered by CountrySelect.Options.
Omit<DropdownContentProps, "children">
Dropdown.Content props, including native popup attributes, ref, render, positioning, portal destination, and keepMounted. Explicit popup accessible names override the trigger name. Children are owned by the picker.
Ref<HTMLButtonElement>
Ref to the trigger element.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, PopoverTriggerState>
Replaces the trigger element while preserving its behavior and props.
string
Placeholder and accessible name of the search input.
Omit<DropdownSearchProps, "defaultValue" | "value" | "onValueChange">
Native search input props, ref, and render. Search value and change handling are owned by the picker.
CSSProperties | ((state: PopoverTriggerState) => CSSProperties | undefined)
Style applied to the element, or a function that
returns a style object based on the component’s state.
string | null
Controlled selected identifier. Null selects no country; undefined leaves selection uncontrolled.
Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "children">
Native props and ref for the div around the trigger and its visible label. Children are owned by the picker.
CountrySelect.Root
ReactNode
required
Picker composition containing Dropdown.Root, CountrySelect.Trigger, and CountrySelect.Options. Root adds no DOM element.
readonly CountryChoice[]
required
Prepared country choices with unique stable string values, caller-owned labels, and optional decorative flags and disabled states. An empty string is a valid choice value.
string | null
default:"null"
Initial uncontrolled selected identifier. Defaults to null.
boolean
default:"false"
Prevents opening and selection. An empty country list is also disabled. The trigger stays focusable with aria-disabled.
string
default:"No country"
Text for the null selection and its option. Defaults to No country.
((value: string | null, eventDetails: DropdownSelectionGroupChangeEventDetails) => void)
Reports the selected identifier or null with event, reason, trigger, cancel and isCanceled details. Call cancel() to preserve selection and keep the popup open.
string | null
Controlled selected identifier. Null selects no country; undefined leaves selection uncontrolled.
CountrySelect.Trigger
ReactNode
Custom trigger content. Replaces the selected country presentation while retaining shared trigger behavior.
string | ((state: PopoverTriggerState) => string | undefined)
Class applied to the trigger button.
number
default:"0"
How long to wait before closing the popover that was opened on hover.
Specified in milliseconds.Requires the
openOnHover prop.number
default:"300"
How long to wait before the popover may be opened on hover. Specified in milliseconds.Requires the
openOnHover prop.string
ID of the trigger. In addition to being forwarded to the rendered element,
it is also used to specify the active trigger for the popover in controlled mode (with the PopoverRoot
triggerId prop).string
Optional visible label that names the trigger.
boolean
default:"true"
Whether the component renders a native
<button> element when replacing it
via the render prop.
Set to false if the rendered element is not a button (e.g. <div>).boolean
default:"false"
Whether the popover should also open when the trigger is hovered.
Ref<HTMLButtonElement>
Ref to the trigger element.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, PopoverTriggerState>
Replaces the trigger element. Custom components must forward the supplied props and ref.
CSSProperties | ((state: PopoverTriggerState) => CSSProperties | undefined)
Style applied to the element, or a function that
returns a style object based on the component’s state.
Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "children">
Native props and ref for the div around the trigger and its visible label. Children are owned by the picker.
CountrySelect.Options
string
Accessible name for the selection listbox. Overrides its default name.
string
Accessible name references for the selection listbox. Defaults to the registered Dropdown title or trigger element.
string
Class applied to the selection group div.
boolean
Closes the popup after a selection. Defaults to true. Cancelled selection keeps it open.
string
Text displayed when no choice matches the search. Defaults to No results.
Ref<HTMLDivElement>
Ref to the selection group element.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, CountrySelectOptionsState>
Replaces the selection group div. Functions receive supplied props and { value, multiple: false } state, with the public string or null selection. Custom components must forward the supplied props and ref.
string
Search placeholder and accessible name. Defaults to Search.
Omit<DropdownSearchProps, "defaultValue" | "value" | "onValueChange">
Native search input props, ref, and render. Search value and change handling are owned by the picker.