Skip to content

Region picker

createRegionList is a headless controller for a region selector: the dropdown a user opens to pick a country calling code. It holds the list of 245 regions, applies your filters, search, and sort, localizes each name, and emits a new snapshot whenever any of that changes. It renders nothing; you own the DOM. It requires the engine to be ready and throws EngineNotReadyError otherwise (see Engine loading).

import { createRegionList } from '@telixon/web-sdk';
const regionList = createRegionList();
regionList.getState().options.length; // 245

Every state change runs one pipeline over the base set and emits the result:

base set (cached per locale) -> regionFilter -> numberTypeFilter -> search -> sort -> prioritize -> emit

The base set is one RegionOption per region, computed once per locale and reused across filter, search, and sort changes. Switching locale (localize) is the only mutation that rebuilds it. A mutator that does not change the underlying value skips the whole pipeline and emits nothing.

Each option carries four fields:

type RegionOption<T = undefined> = {
readonly region: RegionCode; // ISO code, e.g. 'US'
readonly callingCode: string; // numeric calling code as string, e.g. '1'
readonly displayName: string; // localized name from Intl.DisplayNames
readonly data: T; // your per-option payload, or undefined
};

displayName comes from the runtime’s Intl.DisplayNames, so the exact string depends on the platform’s ICU data and is not byte-stable across environments.

Construction does not emit. Read the bootstrap value with getState(), then subscribe for changes. subscribe returns an unsubscribe function.

const regionList = createRegionList();
renderOptions(regionList.getState().options);
const unsubscribe = regionList.subscribe((state) => renderOptions(state.options));

The emitted RegionListState carries the current options plus the active regionFilter, numberTypeFilter, searchQuery, and locale.

setRegionFilter restricts the list to a set of regions; setNumberTypeFilter restricts it to regions supporting at least one of the given number types. Both accept null to remove the restriction. [] is distinct from null: it matches nothing, which lets a caller express “user cleared the filter” versus “narrowed to zero”. Passing an array with the same contents in the same order is a no-op.

regionList.setRegionFilter(['US', 'CA', 'GB']);
regionList.getState().options.length; // 3
regionList.setNumberTypeFilter(['TOLL_FREE']);
regionList.getState().options.map((option) => option.region); // ['US', 'CA', 'GB']
regionList.setRegionFilter(null); // all regions supporting TOLL_FREE
regionList.setNumberTypeFilter(null); // all 245 regions

Pass either filter at construction with regionFilter / numberTypeFilter. These are the same values createPhoneInput accepts, so a picker can offer exactly the regions the input resolves. See Input and picker sync.

search(query) filters by the query and re-emits. An empty or whitespace-only query is short-circuited: the filter is skipped entirely and the full set passes through.

The default matcher tests, in order, the localized displayName, the region code, and the callingCode. It is diacritic- and case-insensitive: it normalizes both sides with Unicode NFD, strips combining marks, and lowercases, so "osterreich" matches "Österreich". A leading + on the query is stripped before the calling-code test, so "+44" and "44" both match the United Kingdom.

regionList.search('united');
// matches United States, United Kingdom, United Arab Emirates, ...
regionList.search('+380');
regionList.getState().options.map((option) => option.region); // ['UA']

To change what a match means, pass a searchFn. It receives the raw query and one option and returns true to keep it. The library still short-circuits empty and whitespace-only queries and never calls your function for them, so you own only the non-empty case. A matcher that searches by calling code exactly:

const regionList = createRegionList({
searchFn: (query, option) => option.callingCode === query.replace(/^\+/, ''),
});
regionList.search('+1');
regionList.getState().options.map((option) => option.region);
// every region on calling code 1: US, CA, and the North American Numbering Plan territories

sort sets the order. Built-in comparators collate displayName in the list’s own locale (not the host default) and break every tie by region code, so they are total orders: the same input produces the same order run to run within one runtime.

sort Order
'alphabetical' by displayName, then region code (default)
'callingCode' by numeric calling code, then displayName, then region code
a function your comparator; you own its determinism
const byCallingCode = createRegionList({ sort: 'callingCode' });
byCallingCode
.getState()
.options.slice(0, 3)
.map((option) => option.callingCode);
// ['1', '1', '1']: calling code 1 sorts first, then 7, 20, ...
const byRegionCode = createRegionList({
sort: (a, b) => a.region.localeCompare(b.region),
});

Because displayName and collation both come from the runtime’s ICU, the alphabetical order is locale- and ICU-dependent and not guaranteed identical across environments.

prioritize pins regions to the top, in the order given, ahead of the sorted rest. A pinned region that a filter or search has removed does not reappear; prioritization reorders the surviving options, it does not add any.

const regionList = createRegionList({
sort: 'alphabetical',
prioritize: ['US', 'GB', 'UA'],
});
regionList
.getState()
.options.slice(0, 5)
.map((option) => option.region);
// ['US', 'GB', 'UA', 'AF', 'AX']: pinned three first, then alphabetical by name

prioritize is a construction option and does not change at runtime.

locale selects the language for every displayName, resolved through Intl.DisplayNames. localize(nextLocale) switches it, rebuilds the base set, recomputes every name and every data value, then re-runs the pipeline. It is a no-op when the locale is unchanged. The default is 'en'.

const regionList = createRegionList({ locale: 'fr' });
regionList.getState().options.find((option) => option.region === 'DE')?.displayName; // 'Allemagne'
regionList.localize('en');
regionList.getState().options.find((option) => option.region === 'DE')?.displayName; // 'Germany'

When the runtime cannot produce a localized name for a region, displayName falls back to the ISO region code.

dataFactory fills the data slot on each option. It runs once per region per base recomputation (construction and localize), receives the region’s already-computed region, callingCode, displayName, and the active locale, and returns your payload. Its return type flows into the list’s generic T, so data is typed for every downstream consumer. Keep it pure; it must not read mutable external state unless you call refresh when that state changes.

import { createRegionList, regionToFlagEmoji } from '@telixon/web-sdk';
const regionList = createRegionList({
dataFactory: ({ region, callingCode }) => ({
flag: regionToFlagEmoji(region),
dialPrefix: `+${callingCode}`,
}),
});
const us = regionList.getState().options.find((option) => option.region === 'US');
us?.data; // { flag: '🇺🇸', dialPrefix: '+1' }

refresh recomputes the base set (display names and data) and always emits, even when nothing changed. Use it when external state your dataFactory reads has moved.

regionToFlagEmoji maps a region code to its flag by returning the two Unicode regional indicator symbols for its letters. regionToFlagEmoji('US') returns '🇺🇸'. Rendering is the platform’s choice: systems with flag-emoji support show a flag, and systems without it (notably Windows) show the two letters. For a flag on every platform, render an SVG set keyed by the region code instead.

import { regionToFlagEmoji, type RegionOption } from '@telixon/web-sdk';
function renderOptions(options: readonly RegionOption[]): void {
select.replaceChildren(
...options.map((option) => {
const element = document.createElement('option');
element.value = option.region;
element.textContent = `${regionToFlagEmoji(option.region)} ${option.displayName} +${option.callingCode}`;
return element;
}),
);
}

destroy clears all subscribers and is idempotent. Call it when the picker unmounts.