Phone Input
Country selector and national number field that produces an E.164-formatted value, with a filterable listbox of 30 common countries.
Preview
- No countries found.
<x-ui.phone-input name="phone" default-country="GB" class="w-80" />
Installation
php artisan ui:add phone-input
Note
This component ships an Alpine behavior module at
resources/js/ui/phone-input.js. Import it once from your bundle so it registers on alpine:init:
import './phone-input.js';
Registry contract
php artisan ui:add phone-input
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/phone-input.blade.php -
resources/js/ui/phone-input.js
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2npm: alpinejs
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Phone Input (`phone-input`)
Country selector and national number field that produces an E.164-formatted value, with a filterable listbox of 30 common countries.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add phone-input
```
## Usage
```blade
<x-ui.phone-input name="phone" default-country="GB" class="w-80" />
```
## Props
- `name` (mixed|null, default `null`) — Form field name; inherited from a wrapping field via @aware or set directly. Names the hidden input that carries the E.164 value.
- `value` (string, default ``) — Initial number; re-parsed on boot into a country and national part.
- `defaultCountry` (string, default `US`) — ISO country code preselected when no value is given.
- `placeholder` (string, default `Phone number`) — Placeholder text shown in the national number field.
## Use when
- Use when formatting, validation, or constrained entry meaningfully reduces errors, such as OTP, phone, currency, password, or numeric entry.
- Collecting a phone number that must submit as a valid E.164 value, with a country dial code the user can change.
- Needing inline SVG country flags with no emoji font dependency.
## Avoid when
- Do not add specialized behavior if a plain field would be simpler and more forgiving.
- The country is always fixed and known; use input type="tel" with a static prefix instead.
- A number that is not a contactable phone number, such as an internal extension format; use input or masked-input instead.
## Anti-patterns
- Adding formatting behavior that makes correction harder
## Rules
- Use the `<brok:phone-input>` tag (or `<x-ui.phone-input>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/phone-input
- Registry JSON (files, props, contract): https://brokui.dev/r/open/phone-input.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| name | mixed | null | null | Form field name; inherited from a wrapping field via @aware or set directly. Names the hidden input that carries the E.164 value. |
| value | string | Initial number; re-parsed on boot into a country and national part. | |
| defaultCountry | string | US | ISO country code preselected when no value is given. |
| placeholder | string | Phone number | Placeholder text shown in the national number field. |
Slots
default— Unused; the control renders from props only.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- The trigger opens the country list on Enter, Space, ArrowDown, ArrowUp, or any printable key, which seeds the filter.
- Inside the popup, arrow keys, Home and End move the active option through aria-activedescendant while the filter input keeps DOM focus.
- The hidden input, when name is set, always mirrors the E.164-formatted value so it submits correctly with any HTML form.
- Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
- Declares registry capability flags: a11y, interactive, responsive, rtl, darkMode, localized.
Guidance
Collect constrained information with error-reducing formatting.
Use when
- Use when formatting, validation, or constrained entry meaningfully reduces errors, such as OTP, phone, currency, password, or numeric entry.
- Collecting a phone number that must submit as a valid E.164 value, with a country dial code the user can change.
- Needing inline SVG country flags with no emoji font dependency.
Avoid when
- Do not add specialized behavior if a plain field would be simpler and more forgiving.
- The country is always fixed and known; use input type="tel" with a static prefix instead.
- A number that is not a contactable phone number, such as an internal extension format; use input or masked-input instead.
Use instead
- Plain text or number field with validation
Anti-patterns
- Adding formatting behavior that makes correction harder
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- Tab Enter Escape ArrowUp ArrowDown Home End
- Focus
managed
- The trigger carries aria-haspopup="listbox" and aria-expanded; Escape closes the popup and returns focus to the trigger.
- Country flags are decorative and aria-hidden; the accessible information is the country name and dial code read from the option text.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="phone-input-{{ $record->id }}">
<x-ui.phone-input name="phone" default-country="GB" class="w-80" />
</div>
Validation
Validation support: laravel-error-bag. Keep the error message connected with aria-describedby.
<form wire:submit="save" class="space-y-2">
<brok:phone-input
wire:model="value"
:aria-invalid="$errors->has('value') ? 'true' : 'false'"
aria-describedby="value-error"
/>
@error('value')
<p id="value-error" role="alert">{{ $message }}</p>
@enderror
<brok:button type="submit" wire:loading.attr="disabled">
<span wire:loading.remove>Save</span>
<span wire:loading>Saving…</span>
</brok:button>
</form>
Source
The exact, editable files ui:add writes
into your app. Previews render this same code; there are no preview-only components.
{{-- A wrapping <x-ui.field name="…"> shares its name down via @aware so this
composite self-wires old()/aria-invalid/aria-describedby off the shared
$errors bag. Inert with no field ancestor and no own `name` —
fully backward-compatible. --}}
@aware([
'name' => null,
])
@props([
'name' => null,
'value' => '',
'defaultCountry' => 'US',
'placeholder' => 'Phone number',
])
@php
$awareName = $name ?? null;
$fieldName = $awareName ?? $attributes->get('name');
$bag = ($errors ?? null) instanceof \Illuminate\Support\ViewErrorBag ? $errors : null;
$hasBagError = $bag && filled($fieldName) && $bag->has($fieldName);
// old() repopulates the entered number after a failed redirect-back. The JS
// re-parses it back into the country + national parts on boot.
$resolvedValue = filled($fieldName) ? old($fieldName, $value) : $value;
$describedBy = (filled($awareName) && $hasBagError) ? $fieldName.'-error' : null;
@endphp
{{--
Phone Input — country selector + national number → E.164 value.
Bundles a filterable listbox of ~30 common countries with inline SVG flags
(no emoji font dependency). The hidden input
(when `name` is set) always mirrors the E.164-formatted value so it
submits correctly via any HTML form.
Keyboard: the trigger opens on Enter/Space/ArrowDown/ArrowUp (and on any
printable key, which seeds the filter — type-ahead). Inside the popup the
filter input keeps DOM focus while Arrow Up/Down, Home and End move the
active option through `aria-activedescendant`; Enter selects it and Escape
closes and returns focus to the trigger.
--}}
<div
x-data="uiPhoneInput({ defaultCountry: @js($defaultCountry), number: @js($resolvedValue) })"
x-id="['phone-input-listbox', 'phone-input-option']"
data-slot="phone-input"
@if ($hasBagError) data-invalid="true" @endif
@click.outside="close(false)"
@keydown.escape="if (open) { $event.stopPropagation(); close() }"
{{ $attributes->except(['name'])->merge(['class' => 'relative flex rounded-md border border-input bg-background focus-within:ring-2 focus-within:ring-ring']) }}
>
@if (filled($fieldName))
<input type="hidden" name="{{ $fieldName }}" :value="e164()" />
@endif
{{-- Country selector trigger --}}
<button
type="button"
data-slot="phone-input-trigger"
x-ref="trigger"
aria-haspopup="listbox"
:aria-expanded="open"
:aria-controls="$id('phone-input-listbox')"
aria-label="{{ __('Select country') }}"
@click="open ? close() : openList()"
@keydown="onTriggerKey($event)"
class="inline-flex shrink-0 items-center gap-2 rounded-s-md border-e border-input bg-background px-3 text-sm text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none"
>
<span x-html="country?.flag" aria-hidden="true" class="inline-block size-5 shrink-0 overflow-hidden rounded-full [&>svg]:block [&>svg]:size-full"></span>
<span x-text="country?.dial" class="text-muted-foreground"></span>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4 shrink-0 text-muted-foreground"
:class="open ? 'rotate-180' : ''"
>
<path d="m6 9 6 6 6-6" />
</svg>
</button>
{{-- Filterable country listbox --}}
<div
data-slot="phone-input-listbox-wrapper"
x-show="open"
x-cloak
x-transition:enter="motion-safe:transition motion-safe:duration-100"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
class="absolute start-0 top-full z-dropdown mt-1 w-64 overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md"
>
{{-- Search filter --}}
<div class="border-b border-border p-2">
<input
type="text"
x-ref="filter"
x-model="query"
role="combobox"
aria-autocomplete="list"
:aria-expanded="open"
:aria-controls="$id('phone-input-listbox')"
:aria-activedescendant="activeIndex >= 0 ? $id('phone-input-option', activeIndex + 1) : null"
@input="activeIndex = 0"
@keydown="onKey($event)"
placeholder="{{ __('Search country…') }}"
autocomplete="off"
aria-label="{{ __('Filter countries') }}"
class="h-8 w-full rounded-sm border border-input bg-background px-2 text-sm text-foreground outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring"
/>
</div>
{{-- Options list --}}
<ul
role="listbox"
data-slot="phone-input-listbox"
x-ref="list"
:id="$id('phone-input-listbox')"
aria-label="{{ __('Countries') }}"
class="max-h-56 overflow-y-auto p-1"
>
<template x-for="(c, i) in filtered()" :key="c.code">
<li
role="option"
:id="$id('phone-input-option', i + 1)"
:data-index="i"
:data-active="i === activeIndex"
:aria-selected="country?.code === c.code"
@click="select(c)"
@mouseenter="activeIndex = i"
class="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-2 text-sm text-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground motion-reduce:transition-none"
:class="(i === activeIndex || country?.code === c.code) ? 'bg-accent text-accent-foreground' : ''"
>
<span x-html="c.flag" aria-hidden="true" class="inline-block size-5 shrink-0 overflow-hidden rounded-full [&>svg]:block [&>svg]:size-full"></span>
<span x-text="c.name" class="grow truncate"></span>
<span x-text="c.dial" class="shrink-0 text-muted-foreground"></span>
</li>
</template>
<li role="presentation" x-show="filtered().length === 0" class="px-2 py-6 text-center text-sm text-muted-foreground">
{{ __('No countries found.') }}
</li>
</ul>
</div>
{{-- National number input --}}
<input
type="tel"
inputmode="tel"
data-slot="phone-input-number"
x-ref="numberInput"
x-model="number"
:placeholder="@js($placeholder)"
@if ($hasBagError) aria-invalid="true" @endif
@if (filled($describedBy)) aria-describedby="{{ $describedBy }}" @endif
autocomplete="tel-national"
class="h-10 min-w-0 grow rounded-e-md bg-background px-3 text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
/>
</div>
/**
* Phone Input behavior.
*
* Bundles a small dataset of ~30 common countries, each with an inline SVG
* flag. The country selector opens a filterable listbox; selecting a country
* updates the dial prefix. The
* national-number input accepts free-form digits. `e164()` returns the
* E.164-style value: dial code + stripped national digits.
*/
/**
* Flag artwork: circle-flags by HatScripts (MIT, https://github.com/HatScripts/circle-flags),
* simplified round renditions inlined so the control ships self-contained. Each
* mask id is namespaced per country so several flags can share one document.
*/
const FLAGS = {
us: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-us-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-us-a)"><path fill="#eee" d="M256 0h256v64l-32 32 32 32v64l-32 32 32 32v64l-32 32 32 32v64l-256 32L0 448v-64l32-32-32-32v-64z"/><path fill="#d80027" d="M224 64h288v64H224Zm0 128h288v64H256ZM0 320h512v64H0Zm0 128h512v64H0Z"/><path fill="#0052b4" d="M0 0h256v256H0Z"/><path fill="#eee" d="m187 243 57-41h-70l57 41-22-67zm-81 0 57-41H93l57 41-22-67zm-81 0 57-41H12l57 41-22-67zm162-81 57-41h-70l57 41-22-67zm-81 0 57-41H93l57 41-22-67zm-81 0 57-41H12l57 41-22-67Zm162-82 57-41h-70l57 41-22-67Zm-81 0 57-41H93l57 41-22-67zm-81 0 57-41H12l57 41-22-67Z"/></g></svg>',
gb: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-gb-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-gb-a)"><path fill="#eee" d="m0 0 8 22-8 23v23l32 54-32 54v32l32 48-32 48v32l32 54-32 54v68l22-8 23 8h23l54-32 54 32h32l48-32 48 32h32l54-32 54 32h68l-8-22 8-23v-23l-32-54 32-54v-32l-32-48 32-48v-32l-32-54 32-54V0l-22 8-23-8h-23l-54 32-54-32h-32l-48 32-48-32h-32l-54 32L68 0H0z"/><path fill="#0052b4" d="M336 0v108L444 0Zm176 68L404 176h108zM0 176h108L0 68ZM68 0l108 108V0Zm108 512V404L68 512ZM0 444l108-108H0Zm512-108H404l108 108Zm-68 176L336 404v108z"/><path fill="#d80027" d="M0 0v45l131 131h45L0 0zm208 0v208H0v96h208v208h96V304h208v-96H304V0h-96zm259 0L336 131v45L512 0h-45zM176 336 0 512h45l131-131v-45zm160 0 176 176v-45L381 336h-45z"/></g></svg>',
ca: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-ca-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-ca-a)"><path fill="#d80027" d="M0 0v512h144l112-64 112 64h144V0H368L256 64 144 0Z"/><path fill="#eee" d="M144 0h224v512H144Z"/><path fill="#d80027" d="m301 289 44-22-22-11v-22l-45 22 23-44h-23l-22-34-22 33h-23l23 45-45-22v22l-22 11 45 22-12 23h45v33h22v-33h45z"/></g></svg>',
au: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-au-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-au-a)"><path fill="#0052b4" d="M0 0h512v512H0z"/><path fill="#eee" d="m154 300 14 30 32-8-14 30 25 20-32 7 1 33-26-21-26 21 1-33-33-7 26-20-14-30 32 8zm222-27h47l-38 27 15-44 14 44zm7-162 7 15 16-4-7 15 12 10-15 3v17l-13-11-13 11v-17l-15-3 12-10-7-15 16 4zm57 67 7 15 16-4-7 15 12 10-15 3v16l-13-10-13 11v-17l-15-3 12-10-7-15 16 4zm-122 22 7 15 16-4-7 15 12 10-15 3v16l-13-10-13 11v-17l-15-3 12-10-7-15 16 4zm65 156 7 15 16-4-7 15 12 10-15 3v17l-13-11-13 11v-17l-15-3 12-10-7-15 16 4zM0 0v32l32 32L0 96v160h32l32-32 32 32h32v-83l83 83h45l-8-16 8-15v-14l-83-83h83V96l-32-32 32-32V0H96L64 32 32 0Z"/><path fill="#d80027" d="M32 0v32H0v64h32v160h64V96h160V32H96V0Zm96 128 128 128v-31l-97-97z"/></g></svg>',
de: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-de-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-de-a)"><path fill="#ffda44" d="m0 345 256.7-25.5L512 345v167H0z"/><path fill="#d80027" d="m0 167 255-23 257 23v178H0z"/><path fill="#333" d="M0 0h512v167H0z"/></g></svg>',
fr: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-fr-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-fr-a)"><path fill="#eee" d="M167 0h178l25.9 252.3L345 512H167l-29.8-253.4z"/><path fill="#0052b4" d="M0 0h167v512H0z"/><path fill="#d80027" d="M345 0h167v512H345z"/></g></svg>',
es: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-es-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-es-a)"><path fill="#ffda44" d="m0 128 256-32 256 32v256l-256 32L0 384Z"/><path fill="#eee" d="M196 168q-11 1-15 11l-5-1q-15 1-16 16c-1 15 7 16 16 16q11 0 15-11a16 16 0 0 0 17-4 16 16 0 0 0 17 4 16 16 0 1 0 10-20 16 16 0 0 0-27-5q-4-6-12-6m0 8q8 1 8 8 0 8-8 8-7 0-8-8 1-7 8-8m24 0q8 1 8 8 0 8-8 8-7 0-8-8 1-7 8-8m-44 10 4 1 4 8q-1 7-8 7-9 0-8-8 1-7 8-8m64 0q8 1 8 8 0 8-8 8-7 0-8-7l4-8zm-112 38v80h16v-80zm80 0v40c-26 0-48 14-48 32s22 32 48 32 48-14 48-32v-72zm64 0v80h16v-80z"/><path fill="#ff9811" d="M200 160h16v32h-16z"/><path fill="#d80027" d="M0 0v128h512V0zm208 184c-22 0-40 11-40 24l8 8h64l8-8c0-13-18-24-40-24m-72 8a8 8 0 0 0-8 8v8a8 8 0 1 0 16 0v-8a8 8 0 0 0-8-8m144 0a8 8 0 0 0-8 8v8a8 8 0 1 0 16 0v-8a8 8 0 0 0-8-8m-120 32v24h-38a4 4 0 0 0-4 4 4 4 0 0 0 4 4h38v40a24 24 0 0 0 24 24 24 24 0 0 0 24-24 24 24 0 0 0 24 24 24 24 0 0 0 24-24v-24h-48v-48zm72 8a10 10 0 0 0-10 10v12a10 10 0 1 0 20 0v-12a10 10 0 0 0-10-10m24 16v8h38a4 4 0 0 0 4-4 4 4 0 0 0-4-4zm-134 24a4 4 0 0 0-4 4 4 4 0 0 0 4 4h28a4 4 0 0 0 4-4 4 4 0 0 0-4-4zm144 0a4 4 0 0 0-4 4 4 4 0 0 0 4 4h28a4 4 0 0 0 4-4 4 4 0 0 0-4-4zM0 384v128h512V384z"/><path fill="#ffda44" d="M186 196a6 6 0 0 0-6 6 6 6 0 0 0 6 6 6 6 0 0 0 6-6 6 6 0 0 0-6-6m22 0a6 6 0 0 0-6 6 6 6 0 0 0 6 6 6 6 0 0 0 6-6 6 6 0 0 0-6-6m22 0a6 6 0 0 0-6 6 6 6 0 0 0 6 6 6 6 0 0 0 6-6 6 6 0 0 0-6-6"/><path fill="#ff9811" d="M128 208a8 8 0 1 0 0 16h16a8 8 0 1 0 0-16zm144 0a8 8 0 1 0 0 16h16a8 8 0 1 0 0-16zm-96 8v8h64v-8zm-8 16v8h8v16h-8v8h32v-8h-8v-16h8v-8zm-8 40v24q1 12 9 19v-43zm19 0v47h10v-47zm20 0v43q9-7 9-19v-24zm-71 32a8 8 0 1 0 0 16h16a8 8 0 1 0 0-16zm144 0a8 8 0 1 0 0 16h16a8 8 0 1 0 0-16z"/><path fill="#338af3" d="M208 256a16 16 0 0 0-16 16 16 16 0 0 0 16 16 16 16 0 0 0 16-16 16 16 0 0 0-16-16m-80 64a8 8 0 1 0 0 16h16a8 8 0 1 0 0-16zm144 0a8 8 0 1 0 0 16h16a8 8 0 1 0 0-16z"/></g></svg>',
it: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-it-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-it-a)"><path fill="#eee" d="M167 0h178l25.9 252.3L345 512H167l-29.8-253.4z"/><path fill="#6da544" d="M0 0h167v512H0z"/><path fill="#d80027" d="M345 0h167v512H345z"/></g></svg>',
nl: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-nl-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-nl-a)"><path fill="#eee" d="m0 167 253.8-19.3L512 167v178l-254.9 32.3L0 345z"/><path fill="#a2001d" d="M0 0h512v167H0z"/><path fill="#0052b4" d="M0 345h512v167H0z"/></g></svg>',
se: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-se-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-se-a)"><path fill="#0052b4" d="M0 0h133.6l35.3 16.7L200.3 0H512v222.6l-22.6 31.7 22.6 35.1V512H200.3l-32-19.8-34.7 19.8H0V289.4l22.1-33.3L0 222.6z"/><path fill="#ffda44" d="M133.6 0v222.6H0v66.8h133.6V512h66.7V289.4H512v-66.8H200.3V0z"/></g></svg>',
no: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-no-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-no-a)"><path fill="#d80027" d="M0 0h100.2l66.1 53.5L233.7 0H512v189.3L466.3 257l45.7 65.8V512H233.7l-68-50.7-65.5 50.7H0V322.8l51.4-68.5-51.4-65z"/><path fill="#eee" d="M100.2 0v189.3H0v33.4l24.6 33L0 289.5v33.4h100.2V512h33.4l30.6-26.3 36.1 26.3h33.4V322.8H512v-33.4l-24.6-33.7 24.6-33v-33.4H233.7V0h-33.4l-33.8 25.3L133.6 0z"/><path fill="#0052b4" d="M133.6 0v222.7H0v66.7h133.6V512h66.7V289.4H512v-66.7H200.3V0z"/></g></svg>',
dk: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-dk-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-dk-a)"><path fill="#d80027" d="M0 0h133.6l32.7 20.3 34-20.3H512v222.6L491.4 256l20.6 33.4V512H200.3l-31.7-20.4-35 20.4H0V289.4l29.4-33L0 222.7z"/><path fill="#eee" d="M133.6 0v222.6H0v66.8h133.6V512h66.7V289.4H512v-66.8H200.3V0h-66.7z"/></g></svg>',
ie: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-ie-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-ie-a)"><path fill="#eee" d="M167 0h178l25.9 252.3L345 512H167l-29.8-253.4z"/><path fill="#6da544" d="M0 0h167v512H0z"/><path fill="#ff9811" d="M345 0h167v512H345z"/></g></svg>',
pt: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-pt-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-pt-a)"><path fill="#6da544" d="M0 512h167l37.9-260.3L167 0H0z"/><path fill="#d80027" d="M512 0H167v512h345z"/><circle cx="167" cy="256" r="89" fill="#ffda44"/><path fill="#d80027" d="M116.9 211.5V267a50 50 0 1 0 100.1 0v-55.6H117z"/><path fill="#eee" d="M167 283.8c-9.2 0-16.7-7.5-16.7-16.7V245h33.4v22c0 9.2-7.5 16.7-16.7 16.7z"/></g></svg>',
ch: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-ch-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-ch-a)"><path fill="#d80027" d="M0 0h512v512H0z"/><path fill="#eee" d="M389.6 211.5h-89v-89h-89.1v89h-89v89h89v89h89v-89h89z"/></g></svg>',
at: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-at-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-at-a)"><path fill="#d80027" d="M0 0h512v167l-23.2 89.7L512 345v167H0V345l29.4-89L0 167z"/><path fill="#eee" d="M0 167h512v178H0z"/></g></svg>',
be: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-be-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-be-a)"><path fill="#333" d="M0 0h167l38.2 252.6L167 512H0z"/><path fill="#d80027" d="M345 0h167v512H345l-36.7-256z"/><path fill="#ffda44" d="M167 0h178v512H167z"/></g></svg>',
pl: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-pl-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-pl-a)"><path fill="#d80027" d="m0 256 256.4-44.3L512 256v256H0z"/><path fill="#eee" d="M0 0h512v256H0z"/></g></svg>',
br: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-br-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-br-a)"><path fill="#6da544" d="M0 0h512v512H0z"/><path fill="#ffda44" d="M256 100.2 467.5 256 256 411.8 44.5 256z"/><path fill="#eee" d="M174.2 221a87 87 0 0 0-7.2 36.3l162 49.8a88.5 88.5 0 0 0 14.4-34c-40.6-65.3-119.7-80.3-169.1-52z"/><path fill="#0052b4" d="M255.7 167a89 89 0 0 0-41.9 10.6 89 89 0 0 0-39.6 43.4 181.7 181.7 0 0 1 169.1 52.2 89 89 0 0 0-9-59.4 89 89 0 0 0-78.6-46.8zM212 250.5a149 149 0 0 0-45 6.8 89 89 0 0 0 10.5 40.9 89 89 0 0 0 120.6 36.2 89 89 0 0 0 30.7-27.3A151 151 0 0 0 212 250.5z"/></g></svg>',
mx: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-mx-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-mx-a)"><path fill="#eee" d="M144 0h223l33 256-33 256H144l-32-256z"/><path fill="#d80027" d="M368 0h144v512H368z"/><path fill="#751a46" d="M256 174c22 11 12 33 11 34l-2-4c-4-15-13-33-31-18v11q10 1 11 11-11 12-4 26l4 8-13 23 29-7 18 18v-11l11 11 23-11-35-21-2-13c22-2 34 4 51 29 9-83-45-86-64-86Z"/><path fill="#6da544" d="M209 183q-5 4-4 12 1 11 10 15c3 2 8 0 10 3 3 3-2 5-4 6q-8 5-9 14 3 10 12 15c2 2 7 4 5 7q-4 2-9-2-12-6-19-19c-2-3-1-10-7-10-7 2-4 10-2 14q8 14 21 23 9 8 20 3 10-6 4-17c-3-6-11-8-14-14-2-3 2-4 4-6q8-4 9-11-2-13-14-15-6 1-7-6c-1-3 3-7 0-11q-2-3-6-1"/><path fill="#496e2d" d="M0 0v512h144V0zm164 235a5 5 0 0 0-5 5 97 97 0 0 0 194 0 5 5 0 0 0-5-5 5 5 0 0 0-5 5 87 87 0 1 1-174 0 5 5 0 0 0-5-5m35 25-4 1q-3 4 1 8 23 21 54 24v17h12v-17q31-3 54-24 4-4 1-8-4-3-8 0a78 78 0 0 1-106 0z"/><path fill="#338af3" d="M256 316q-21 0-40-13l6-9c20 13 48 13 68 0l7 9q-18 13-41 13"/><rect width="34" height="22" x="239" y="299" fill="#ff9811" rx="11" ry="11"/><path fill="#ffda44" d="m234 186-12 11v11l18-9q4-3 1-7zm-62 79a10 10 0 0 0-10 10 10 10 0 0 0 10 10 10 10 0 0 0 10-10 10 10 0 0 0-10-10m169 0a10 10 0 0 0-10 10 10 10 0 0 0 10 10 10 10 0 0 0 10-10 10 10 0 0 0-10-10m-69 4-16 8v5l15-1 4-9zm-83 23a10 10 0 0 0-10 10 10 10 0 0 0 10 10 10 10 0 0 0 10-10 10 10 0 0 0-10-10m135 0a10 10 0 0 0-10 10 10 10 0 0 0 10 10 10 10 0 0 0 10-10 10 10 0 0 0-10-10m-108 21a10 10 0 0 0-10 10 10 10 0 0 0 10 10 10 10 0 0 0 10-10 10 10 0 0 0-10-10m81 0a10 10 0 0 0-10 10 10 10 0 0 0 10 10 10 10 0 0 0 10-10 10 10 0 0 0-10-10"/></g></svg>',
ar: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-ar-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-ar-a)"><path fill="#338af3" d="M0 0h512v144.7L488 256l24 111.3V512H0V367.3L26 256 0 144.7z"/><path fill="#eee" d="M0 144.7h512v222.6H0z"/><path fill="#ffda44" d="m332.4 256-31.2 14.7 16.7 30.3-34-6.5-4.2 34.3-23.7-25.2-23.6 25.2-4.3-34.3-34 6.5 16.6-30.3-31.2-14.7 31.3-14.7L194 211l34 6.5 4.3-34.3 23.6 25.2 23.6-25.2 4.4 34.3 34-6.5-16.7 30.3z"/></g></svg>',
in: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-in-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-in-a)"><path fill="#eee" d="m0 160 256-32 256 32v192l-256 32L0 352z"/><path fill="#ff9811" d="M0 0h512v160H0Z"/><path fill="#6da544" d="M0 352h512v160H0Z"/><circle cx="256" cy="256" r="72" fill="#0052b4"/><circle cx="256" cy="256" r="48" fill="#eee"/><circle cx="256" cy="256" r="24" fill="#0052b4"/></g></svg>',
cn: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-cn-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-cn-a)"><path fill="#d80027" d="M0 0h512v512H0z"/><path fill="#ffda44" d="m140.1 155.8 22.1 68h71.5l-57.8 42.1 22.1 68-57.9-42-57.9 42 22.2-68-57.9-42.1H118zm163.4 240.7-16.9-20.8-25 9.7 14.5-22.5-16.9-20.9 25.9 6.9 14.6-22.5 1.4 26.8 26 6.9-25.1 9.6zm33.6-61 8-25.6-21.9-15.5 26.8-.4 7.9-25.6 8.7 25.4 26.8-.3-21.5 16 8.6 25.4-21.9-15.5zm45.3-147.6L370.6 212l19.2 18.7-26.5-3.8-11.8 24-4.6-26.4-26.6-3.8 23.8-12.5-4.6-26.5 19.2 18.7zm-78.2-73-2 26.7 24.9 10.1-26.1 6.4-1.9 26.8-14.1-22.8-26.1 6.4 17.3-20.5-14.2-22.7 24.9 10.1z"/></g></svg>',
jp: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-jp-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-jp-a)"><path fill="#eee" d="M0 0h512v512H0z"/><circle cx="256" cy="256" r="111.3" fill="#d80027"/></g></svg>',
kr: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-kr-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-kr-a)"><path fill="#eee" d="M0 0h512v512H0Z"/><path fill="#333" d="m350 335 24-24 16 16-24 23zm-39 39 24-24 15 16-23 24zm87 8 23-24 16 16-24 24zm-40 39 24-23 16 15-24 24Zm16-63 24-23 15 15-23 24zm-39 40 23-24 16 16-24 23zm63-221-63-63 15-15 64 63zm-63-15-24-24 16-16 23 24zm39 39-24-24 16-15 24 23zm8-87-24-23 16-16 24 24Zm39 40-23-24 15-16 24 24ZM91 358l63 63-16 16-63-63zm63 16 23 24-15 15-24-23zm-40-39 24 23-16 16-23-24zm24-24 63 63-16 16-63-63zm16-220-63 63-16-16 63-63zm23 23-63 63-15-16 63-63zm24 24-63 63-16-16 63-63z"/><path fill="#d80027" d="M319 319 193 193a89 89 0 1 1 126 126z"/><path fill="#0052b4" d="M319 319a89 89 0 1 1-126-126z"/><circle cx="224.5" cy="224.5" r="44.5" fill="#d80027"/><circle cx="287.5" cy="287.5" r="44.5" fill="#0052b4"/></g></svg>',
sg: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-sg-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-sg-a)"><path fill="#d80027" d="M0 0h512v256l-256 32L0 256Z"/><path fill="#eee" d="M200 56a78 78 0 1 0 17 154 78 78 0 0 1 0-152q-8-2-17-2m56 5-5 17h-18l14 11-5 17 14-11 15 11-6-17 15-11h-18zm-43 34-6 17h-18l15 10-6 17 15-10 14 10-5-17 14-10h-18zm86 0-5 17h-18l14 10-5 17 14-10 15 10-6-17 15-10h-18zm-70 50-5 17h-18l14 10-5 17 14-10 15 10-6-17 15-10h-18zm54 0-6 17h-18l15 10-6 17 15-10 14 10-5-17 14-10h-18ZM0 256v256h512V256Z"/></g></svg>',
za: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-za-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-za-a)"><path fill="#eee" d="m0 0 192 256L0 512h47l465-189v-34l-32-33 32-33v-34L47 0Z"/><path fill="#333" d="M0 142v228l140-114z"/><path fill="#ffda44" d="M192 256 0 95v47l114 114L0 370v47z"/><path fill="#6da544" d="M512 223H223L0 0v94l161 162L0 418v94l223-223h289z"/><path fill="#d80027" d="M512 0H47l189 189h276z"/><path fill="#0052b4" d="M512 512H47l189-189h276z"/></g></svg>',
ng: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-ng-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-ng-a)"><path fill="#6da544" d="M0 0v512h160l96-64 96 64h160V0H352l-96 64-96-64Z"/><path fill="#eee" d="M160 0h192v512H160Z"/></g></svg>',
ae: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-ae-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-ae-a)"><path fill="#a2001d" d="M0 0h167l52.3 252L167 512H0z"/><path fill="#eee" d="m167 167 170.8-44.6L512 167v178l-173.2 36.9L167 345z"/><path fill="#6da544" d="M167 0h345v167H167z"/><path fill="#333" d="M167 345h345v167H167z"/></g></svg>',
nz: '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 512 512"><mask id="brok-flag-nz-a"><circle cx="256" cy="256" r="256" fill="#fff"/></mask><g mask="url(#brok-flag-nz-a)"><path fill="#0052b4" d="M256 0h256v512H0V256Z"/><path fill="#eee" d="M0 0v32l32 32L0 96v160h32l32-32 32 32h32v-83l83 83h45l-8-16 8-15v-14l-83-83h83V96l-32-32 32-32V0H96L64 32 32 0Zm382 92-11 35h-37l30 21-12 35 30-22 30 22-12-35 30-21h-37l-11-35Zm61 72-11 35h-37l30 21-11 35 29-21 30 21-12-35 30-21h-37Zm-123 10-11 35h-37l30 22-11 35 29-22 30 22-11-35 29-22h-36zm59 130-11 35h-37l30 21-11 35 29-21 30 21-11-35 29-21h-36z"/><path fill="#d80027" d="M32 0v32H0v64h32v160h64V96h160V32H96V0Zm96 128 128 128v-31l-97-97zm251 201-5 18h-19l15 10-6 18 15-11 15 11-5-18 14-10h-18Zm-59-129-5 17h-19l15 11-6 17 15-11 15 11-6-17 15-11h-18l-6-17zm123-11-6 18h-18l15 11-6 17 15-11 15 11-6-17 15-11h-18l-6-18zm-61-72-6 17h-18l15 11-6 17 15-10 15 10-6-17 15-11h-18z"/></g></svg>',
};
document.addEventListener('alpine:init', () => {
window.Alpine.data('uiPhoneInput', (config = {}) => ({
open: false,
query: '',
activeIndex: 0,
number: config.number ?? '',
countries: [
{ code: 'US', dial: '+1', name: 'United States', flag: FLAGS.us },
{ code: 'GB', dial: '+44', name: 'United Kingdom', flag: FLAGS.gb },
{ code: 'CA', dial: '+1', name: 'Canada', flag: FLAGS.ca },
{ code: 'AU', dial: '+61', name: 'Australia', flag: FLAGS.au },
{ code: 'DE', dial: '+49', name: 'Germany', flag: FLAGS.de },
{ code: 'FR', dial: '+33', name: 'France', flag: FLAGS.fr },
{ code: 'ES', dial: '+34', name: 'Spain', flag: FLAGS.es },
{ code: 'IT', dial: '+39', name: 'Italy', flag: FLAGS.it },
{ code: 'NL', dial: '+31', name: 'Netherlands', flag: FLAGS.nl },
{ code: 'SE', dial: '+46', name: 'Sweden', flag: FLAGS.se },
{ code: 'NO', dial: '+47', name: 'Norway', flag: FLAGS.no },
{ code: 'DK', dial: '+45', name: 'Denmark', flag: FLAGS.dk },
{ code: 'IE', dial: '+353', name: 'Ireland', flag: FLAGS.ie },
{ code: 'PT', dial: '+351', name: 'Portugal', flag: FLAGS.pt },
{ code: 'CH', dial: '+41', name: 'Switzerland', flag: FLAGS.ch },
{ code: 'AT', dial: '+43', name: 'Austria', flag: FLAGS.at },
{ code: 'BE', dial: '+32', name: 'Belgium', flag: FLAGS.be },
{ code: 'PL', dial: '+48', name: 'Poland', flag: FLAGS.pl },
{ code: 'BR', dial: '+55', name: 'Brazil', flag: FLAGS.br },
{ code: 'MX', dial: '+52', name: 'Mexico', flag: FLAGS.mx },
{ code: 'AR', dial: '+54', name: 'Argentina', flag: FLAGS.ar },
{ code: 'IN', dial: '+91', name: 'India', flag: FLAGS.in },
{ code: 'CN', dial: '+86', name: 'China', flag: FLAGS.cn },
{ code: 'JP', dial: '+81', name: 'Japan', flag: FLAGS.jp },
{ code: 'KR', dial: '+82', name: 'South Korea', flag: FLAGS.kr },
{ code: 'SG', dial: '+65', name: 'Singapore', flag: FLAGS.sg },
{ code: 'ZA', dial: '+27', name: 'South Africa', flag: FLAGS.za },
{ code: 'NG', dial: '+234', name: 'Nigeria', flag: FLAGS.ng },
{ code: 'AE', dial: '+971', name: 'United Arab Emirates', flag: FLAGS.ae },
{ code: 'NZ', dial: '+64', name: 'New Zealand', flag: FLAGS.nz },
],
country: null,
init() {
this.country =
this.countries.find((c) => c.code === (config.defaultCountry || 'US')) ||
this.countries[0];
},
filtered() {
const q = this.query.toLowerCase();
if (!q) {
return this.countries;
}
return this.countries.filter(
(c) => c.name.toLowerCase().includes(q) || c.dial.includes(q)
);
},
/**
* Open the popup, park the active option on the current country and
* move DOM focus to the filter input — the listbox itself stays
* unfocused and is driven through `aria-activedescendant`.
*/
openList(seed = '') {
this.open = true;
this.query = seed;
const list = this.filtered();
const current = list.findIndex((c) => c.code === this.country?.code);
this.activeIndex = current >= 0 ? current : 0;
this.$nextTick(() => {
this.$refs.filter?.focus();
this.scrollActiveIntoView();
});
},
/** Close the popup; `restoreFocus` returns focus to the trigger. */
close(restoreFocus = true) {
if (!this.open) {
return;
}
this.open = false;
this.query = '';
this.activeIndex = 0;
if (restoreFocus) {
this.$nextTick(() => this.$refs.trigger?.focus());
}
},
/** Move the active option by `delta`, wrapping at both ends. */
move(delta) {
const count = this.filtered().length;
if (count === 0) {
this.activeIndex = -1;
return;
}
this.activeIndex = (this.activeIndex + delta + count) % count;
this.scrollActiveIntoView();
},
moveTo(index) {
const count = this.filtered().length;
if (count === 0) {
this.activeIndex = -1;
return;
}
this.activeIndex = Math.min(Math.max(index, 0), count - 1);
this.scrollActiveIntoView();
},
scrollActiveIntoView() {
const option = this.$refs.list?.querySelector(
`[data-index="${this.activeIndex}"]`
);
option?.scrollIntoView({ block: 'nearest' });
},
chooseActive() {
const choice = this.filtered()[this.activeIndex];
if (choice) {
this.select(choice);
}
},
/** Keyboard model for the filter input while the listbox is open. */
onKey(event) {
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
this.move(1);
break;
case 'ArrowUp':
event.preventDefault();
this.move(-1);
break;
case 'Home':
event.preventDefault();
this.moveTo(0);
break;
case 'End':
event.preventDefault();
this.moveTo(this.filtered().length - 1);
break;
case 'Enter':
event.preventDefault();
this.chooseActive();
break;
case 'Escape':
event.preventDefault();
this.close();
break;
case 'Tab':
this.close(false);
break;
default:
break;
}
},
/**
* Trigger keyboard model: Enter/Space/Arrow open the popup, and any
* printable character opens it with that character as the filter
* (type-ahead).
*/
onTriggerKey(event) {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
this.openList();
return;
}
if (event.key === 'Escape') {
this.close(false);
return;
}
const printable =
event.key.length === 1 &&
!event.ctrlKey &&
!event.metaKey &&
!event.altKey &&
event.key !== ' ';
if (printable) {
event.preventDefault();
this.openList(event.key);
}
},
select(c) {
this.country = c;
this.open = false;
this.query = '';
this.activeIndex = 0;
this.$nextTick(() => this.$refs.numberInput?.focus());
},
e164() {
return (this.country?.dial || '') + this.number.replace(/[^0-9]/g, '');
},
}));
});
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.2.3 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0