Build a region picker
createRegionList turns the engine’s region data into
picker rows. The RegionList filters, searches, sorts, and prioritizes the list, while your
renderList function draws the rows.
Live demo
Section titled “Live demo”All 245 regions are in the list, with US, CA, and GB pinned on top. Search united and the list
narrows to three rows. The match runs over names, region codes, and calling codes; +44 finds
the United Kingdom too. Click a row and the status reports the selection, while the arrow keys
walk the list from the search box and Enter picks the active row.
The code
Section titled “The code”The demo and the source are the same three files. The markup is a search input, an empty list, and
a status line. createRegionList builds the rows, with
prioritize
pinning US, CA, and GB and a dataFactory putting a
flag emoji in each option’s data slot. The search box
feeds search. renderList rebuilds the rows from each emitted
state.
<div id="region-picker"> <label for="region-picker-search">Region</label> <input id="region-picker-search" type="text" role="combobox" aria-expanded="true" aria-controls="region-picker-listbox" aria-autocomplete="list" placeholder="Search regions" /> <ul id="region-picker-listbox" role="listbox" aria-label="Regions"></ul> <p class="region-picker-status"></p></div>#region-picker { --surface: #ffffff; --text: #1c1f1e; --muted: #66716d; --border: #c9d1ce; --accent: #26997b;
color: var(--text);}
#region-picker label { display: block; margin-bottom: 0.375rem; font-size: 0.875rem; font-weight: 600;}
#region-picker input { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--border); border-radius: 0.375rem; background: var(--surface); color: inherit; font-size: 1rem;}
#region-picker input:focus-visible { border-color: var(--accent); outline: none; box-shadow: 0 0 0 1px var(--accent);}
#region-picker input::placeholder { color: var(--muted);}
#region-picker ul { margin: 0.75rem 0 0; padding: 0.375rem; list-style: none; max-height: 16rem; overflow-y: auto; scrollbar-width: thin; border: 1px solid var(--border); border-radius: 0.5rem; background: var(--surface);}
#region-picker li { display: flex; align-items: center; gap: 0.625rem; padding: 0.5rem 0.75rem; border-radius: 0.375rem; font-size: 0.875rem; cursor: pointer;}
#region-picker li:hover,#region-picker li[data-active='true'] { background: rgb(38 153 123 / 0.08);}
#region-picker li[aria-selected='true'] { background: rgb(38 153 123 / 0.14);}
#region-picker li .region-picker-code { margin-left: auto; color: var(--muted); font-variant-numeric: tabular-nums;}
#region-picker li.region-picker-empty { justify-content: center; color: var(--muted); cursor: default; pointer-events: none;}
#region-picker .region-picker-status { margin: 0.5rem 0 0; min-height: 1.25rem; font-size: 0.875rem; color: var(--muted);}import { ensureEngineReady } from '@telixon/core';import { createRegionList, regionToFlagEmoji } from '@telixon/web-sdk';
await ensureEngineReady();
const search = document.querySelector('#region-picker input');const list = document.querySelector('#region-picker ul');const status = document.querySelector('#region-picker .region-picker-status');
const regions = createRegionList({ prioritize: ['US', 'CA', 'GB'], dataFactory: ({ region }) => regionToFlagEmoji(region),});
let selected = 'US';
// Shown when the search matches nothing.const emptyRow = document.createElement('li');emptyRow.className = 'region-picker-empty';emptyRow.textContent = 'No matches';
// Full rebuild when the search changes the option set.function renderList(state) { if (state.options.length === 0) { list.replaceChildren(emptyRow); return; }
list.replaceChildren( ...state.options.map((option) => { const row = document.createElement('li'); row.id = 'region-picker-option-' + option.region; row.dataset.region = option.region; row.setAttribute('role', 'option'); row.setAttribute('aria-selected', String(option.region === selected));
const flag = document.createElement('span'); flag.textContent = option.data; const name = document.createElement('span'); name.textContent = option.displayName; const code = document.createElement('span'); code.className = 'region-picker-code'; code.textContent = '+' + option.callingCode;
row.append(flag, name, code); return row; }), );}
// Move the highlight without rebuilding the list.function markSelected(region) { const previous = list.querySelector('[aria-selected="true"]'); if (previous) previous.setAttribute('aria-selected', 'false'); const next = list.querySelector(`[data-region="${region}"]`); if (next) next.setAttribute('aria-selected', 'true');}
// The keyboard cursor. Focus stays in the search while aria-activedescendant names the active row.let activeRegion = null;
function setActive(region) { const previous = list.querySelector('[data-active="true"]'); if (previous) previous.removeAttribute('data-active'); search.removeAttribute('aria-activedescendant'); activeRegion = null;
const next = region === null ? null : list.querySelector(`[data-region="${region}"]`); if (next === null) return; activeRegion = region; next.setAttribute('data-active', 'true'); search.setAttribute('aria-activedescendant', next.id); // Scroll the list only; scrollIntoView would drag every scrollable ancestor, the page included. const rowRect = next.getBoundingClientRect(); const listRect = list.getBoundingClientRect(); if (rowRect.top < listRect.top) list.scrollTop += rowRect.top - listRect.top; else if (rowRect.bottom > listRect.bottom) list.scrollTop += rowRect.bottom - listRect.bottom;}
// Wraps past either end; without a cursor, Down starts at the first row and Up at the last.function moveActive(step) { const rows = [...list.children]; if (rows.length === 0) return; const index = rows.findIndex((row) => row.dataset.region === activeRegion); const nextIndex = index === -1 ? (step > 0 ? 0 : rows.length - 1) : (index + step + rows.length) % rows.length; setActive(rows[nextIndex].dataset.region);}
function select(region) { const option = regions.getState().options.find((candidate) => candidate.region === region); if (option === undefined) return; selected = region; markSelected(region); status.textContent = 'Selected: ' + option.displayName + ' (+' + option.callingCode + ')';}
search.addEventListener('input', () => regions.search(search.value));search.addEventListener('keydown', (event) => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); moveActive(event.key === 'ArrowDown' ? 1 : -1); return; } if (event.key === 'Enter' && activeRegion !== null) { event.preventDefault(); select(activeRegion); }});list.addEventListener('click', (event) => { const row = event.target.closest('li'); if (row) select(row.dataset.region);});
// A new option set moves the cursor to the first row, which lets Enter pick the top match.regions.subscribe((state) => { renderList(state); setActive(state.options.length > 0 ? state.options[0].region : null);});
// The subscription fires only on later changes; render the initial state manually.renderList(regions.getState());status.textContent = 'Selected: United States (+1)';The picker so far only reports the selection. Build the complete field wires it to a phone input.