Build a region picker
createRegionList turns the engine’s region data into
picker rows. createRegionPicker holds the open
state, the keyboard cursor, and the selection.
attachRegionPicker wires a button, a search box,
and a list to it, while your render functions draw the rows and the button.
Live demo
Section titled “Live demo”Open the picker. 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. Pick a row and the button shows its flag and name, with
the status line reporting the selection. The arrow keys walk the rows from the search box, Enter
picks the active row, and Escape closes the list.
The code
Section titled “The code”The demo and the source are the same three files. The markup is a button with a flag sprite cell and a name, a popup with the search box and an empty list, and a status line.
The script creates two widgets. createRegionList builds the rows, with
prioritize pinning US, CA, and GB. createRegionPicker
takes the list and, without a phone input, keeps the selection itself.
select seeds it with the United States.
attachRegionPicker wires the button, the search box, and the list to the picker and owns the
clicks, the keys, the outside press, and the ARIA attributes. The demo hands it three functions.
renderOption builds a row from a flag cell, the name, and the calling code, with
flagTransform selecting the cell. renderEmpty builds the row
shown when the search matches nothing. renderTrigger puts the selected region’s flag and name on
the button and writes the status line.
<div id="region-picker"> <label for="region-picker-trigger">Region</label>
<button id="region-picker-trigger" class="region-picker-trigger"> <span class="tlx-flag" aria-hidden="true"><span class="tlx-flag__image"></span></span> <span class="region-picker-name"></span> <svg class="region-picker-chevron" viewBox="0 0 10 6" aria-hidden="true"> <path d="M1 1l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5" /> </svg> </button>
<div class="region-picker-menu" hidden> <input class="region-picker-search" type="text" aria-label="Search regions" placeholder="Search regions" /> <ul aria-label="Regions"></ul> </div>
<p class="region-picker-status"></p></div>#region-picker { --surface: #ffffff; --text: #1c1f1e; --muted: #66716d; --border: #c9d1ce; --accent: #26997b;
position: relative; color: var(--text);}
#region-picker label { display: block; margin-bottom: 0.375rem; font-size: 0.875rem; font-weight: 600;}
#region-picker .region-picker-trigger { display: flex; align-items: center; gap: 0.5rem; width: min(24rem, 100%); padding: 0.5rem 0.75rem; border: 1px solid var(--border); border-radius: 0.375rem; background: var(--surface); color: inherit; font-size: 1rem; font-family: inherit; text-align: left; cursor: pointer;}
#region-picker .region-picker-trigger:hover { background: color-mix(in srgb, var(--text) 5%, transparent);}
#region-picker .region-picker-trigger:focus-visible { border-color: var(--accent); outline: none; box-shadow: 0 0 0 1px var(--accent);}
#region-picker .tlx-flag { width: 1.25rem; border-radius: 2px;}
#region-picker .region-picker-name { flex: 1;}
#region-picker .region-picker-chevron { width: 0.625rem; height: 0.375rem; color: var(--muted);}
@media (prefers-reduced-motion: no-preference) { #region-picker .region-picker-chevron { transition: transform 150ms ease; }}
#region-picker .region-picker-trigger[aria-expanded='true'] .region-picker-chevron { transform: rotate(180deg);}
#region-picker .region-picker-menu { position: absolute; z-index: 10; margin-top: 0.375rem; width: min(24rem, 100%); padding: 0.5rem; border: 1px solid var(--border); border-radius: 0.5rem; background: var(--surface); box-shadow: 0 1px 2px rgb(0 0 0 / 0.06), 0 8px 24px rgb(0 0 0 / 0.1);}
#region-picker .region-picker-menu input { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--border); border-radius: 0.375rem; background: var(--surface); color: inherit; font-size: 0.875rem;}
#region-picker .region-picker-menu 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.5rem 0 0; padding: 0; list-style: none; max-height: 15rem; overflow-y: auto; scrollbar-width: thin;}
#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[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 { attachRegionPicker, createRegionList, createRegionPicker } from '@telixon/web-sdk';import { flagTransform } from '@telixon/web-sdk/flags';
await ensureEngineReady();
// The button with its flag and name, the popup with the search box and the list, and the status line.const root = document.querySelector('#region-picker');const trigger = root.querySelector('.region-picker-trigger');const flag = trigger.querySelector('.tlx-flag__image');const name = trigger.querySelector('.region-picker-name');const popup = root.querySelector('.region-picker-menu');const search = root.querySelector('.region-picker-search');const listbox = root.querySelector('ul');const status = root.querySelector('.region-picker-status');
// The list is the material. The picker holds the open state, the keyboard cursor, and the selection.const regions = createRegionList({ prioritize: ['US', 'CA', 'GB'] });const picker = createRegionPicker({ regions });
// One sprite cell. The transform brings the region's flag into view.function flagCell(region) { const cell = document.createElement('span'); cell.className = 'tlx-flag'; cell.setAttribute('aria-hidden', 'true'); const image = document.createElement('span'); image.className = 'tlx-flag__image'; image.style.transform = flagTransform(region); cell.append(image); return cell;}
// A row is a flag, a name, and a calling code. The adapter adds the option attributes.function renderOption(option) { const row = document.createElement('li'); const rowName = document.createElement('span'); rowName.textContent = option.displayName; const code = document.createElement('span'); code.className = 'region-picker-code'; code.textContent = '+' + option.callingCode; row.append(flagCell(option.region), rowName, code); return row;}
// Shown when the search matches nothing.function renderEmpty() { const row = document.createElement('li'); row.className = 'region-picker-empty'; row.textContent = 'No matches'; return row;}
// The button and the status line show the selection.function renderTrigger(selected) { flag.style.transform = flagTransform(selected === null ? null : selected.region); name.textContent = selected === null ? 'Select a region' : selected.displayName; status.textContent = selected === null ? 'Nothing selected.' : `Selected: ${selected.displayName} (+${selected.callingCode})`;}
// Start on the United States. Without a phone, the picker keeps the selection itself.picker.select('US');
// The adapter wires the DOM. renderTrigger runs once now and again on every change of the selection.attachRegionPicker({ picker, trigger, popup, search, listbox, renderOption, renderEmpty, renderTrigger });The picker so far only reports the selection. Build the complete field hands it a phone input, which then owns the region.