Skip to content

Build the complete field

This guide wires the phone field and the region picker into one widget, where the picker supplies the calling code and the input holds the rest.

Open the picker, search canada, and pick it. The button shows the Canadian flag on +1, with the empty field’s placeholder now a Canadian example. Type 6045550132 and the field shows 604-555-0132 with the status Valid: +16045550132. Reopen the picker and choose United States. 604 still belongs to Canada, which flips the flag straight back.

Undo reaches the picker as well. Cmd/Ctrl+Z steps back through the digits, then through the region change itself, carrying the flag and the calling code with it. Cmd/Ctrl+Shift+Z replays the whole sequence.

The keyboard works throughout. Search united k and press Enter, or walk the rows with the arrow keys. Select the United Kingdom and type 07400123456. The leading zero is a trunk prefix with no place after +44. The status reads NATIONAL_PREFIX_PRESENT.

The demo and the source are the same three files. The markup is the region button, the input, and a popup with the search box and the list. callingCodeInInput: false keeps the calling code out of the input, leaving the selected region to supply it.

The wiring runs both ways. A click on a row calls select, which updates the button, hands the region to setRegion, closes the popup, and returns focus to the input. Coming back, the subscriber follows the resolved state.region, moving the flag when the digits land in another region on the same calling code.

The search box doubles as the keyboard cursor, where the arrow keys walk the rows through aria-activedescendant and Enter picks the active one. Escape closes the popup and hands focus back to the region button, while an outside press or focus moving out of the widget closes it too.

Inside a framework component, call each one’s destroy on unmount and remove the document-level press listener with them.

<div id="complete-field" class="phone-field">
<label for="phone-field-input">Phone number</label>
<div class="phone-field-control">
<button
class="phone-field-region"
type="button"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="phone-field-listbox"
aria-label="Select region"
>
<span class="phone-field-flag"></span>
<span class="phone-field-code"></span>
<svg class="phone-field-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>
<input id="phone-field-input" type="tel" />
</div>
<div class="phone-field-menu" hidden>
<input
class="phone-field-search"
type="text"
role="combobox"
aria-expanded="true"
aria-controls="phone-field-listbox"
aria-autocomplete="list"
aria-label="Search regions"
placeholder="Search regions"
/>
<ul id="phone-field-listbox" role="listbox" aria-label="Regions"></ul>
</div>
<p class="phone-field-status"></p>
</div>

PhoneInput and RegionList never learn about each other. All the wiring lives in the demo’s own functions.