Input phone
Input phone is a field for collecting an international phone number, rendered as two adjacent parts: a compact, flag-only country selector on the leading side that opens a searchable dropdown, and the phone field itself on the trailing side. Typed digits are grouped for display as you type, and the country dropdown opens using the same positioning behavior as Select's own dropdown.
No phone-number library is used. The country and dial-code list is a small, hand-maintained table, and the digits shown while typing are grouped with a generic, non-authoritative heuristic, not real per-country formatting. Typing is capped at a generous upper bound sourced from public numbering plans, not a validated length, so this is not true validation. If strict validation or locale-accurate formatting matters, validate the emitted value server-side, or with a real phone-number library, rather than relying on this component alone.
Dependencies
When to use
Use input phone to collect an international phone number where the
person filling the form may need to pick a country other than your
application's default. For a single, known country with no need for a
country selector, a plain
Install & usage
Pick a framework in the toolbar above and these snippets adapt.
npm install @eunomia/elements
import "@eunomia/elements/input-phone.js";
import "@eunomia/elements/label.js";
<eun-input-phone label="Phone number" country="FR"></eun-input-phone>
document
.querySelector("eun-input-phone")
.addEventListener("eunchange", (event) => {
console.log(event.target.value); // e.g. "+33612345678"
});
Importing the file registers <eun-input-phone> as a custom element, with
no further setup needed. It works with any framework, or none, since it's a
standard web component.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/input-phone.js";
import "@eunomia/elements/label.js";
</script>
<eun-input-phone label="Phone number" country="FR"></eun-input-phone>
<script>
document
.querySelector("eun-input-phone")
.addEventListener("eunchange", (event) => {
console.log(event.target.value);
});
</script>
npm install @eunomia/elements
import "@eunomia/elements/input-phone.js";
import "@eunomia/elements/label.js";
function PhoneField() {
return (
<eun-input-phone
label="Phone number"
country="FR"
oneunchange={(event) => console.log(event.target.value)}
/>
);
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/input-phone.js";
import "@eunomia/elements/label.js";
export function PhoneField() {
return (
<eun-input-phone
label="Phone number"
country="FR"
oneunchange={(event) => console.log(event.target.value)}
/>
);
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/input-phone.js";
import "@eunomia/elements/label.js";
</script>
<template>
<eun-input-phone
label="Phone number"
country="FR"
@eunchange="(event) => console.log(event.target.value)"
/>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/input-phone.js";
import "@eunomia/elements/label.js";
@Component({
selector: "app-phone-field",
template: `
<eun-input-phone
label="Phone number"
country="FR"
(eunchange)="onChange($event)"
></eun-input-phone>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class PhoneFieldComponent {
onChange(event: Event) {
console.log((event.target as HTMLElement & { value: string }).value);
}
}
Alternatives
eun-inputGuidance
- Set
countryordefault-countryto whatever your application already knows about the person (billing address, browser locale), so the selector starts on the right flag instead of the generic globe - Treat the emitted
value(E.164) as unvalidated beyond a basic length cap: pair it with a server-side check (or a real phone-number library) wherever correctness genuinely matters, e.g. before sending an SMS - Restrict the dropdown with slotted
<option>elements when only a handful of countries are ever realistically valid for your form (e.g. a regional service), so the person isn't scrolling through ~195 countries to find one of three - See Alternatives above when there's no need for a country selector at all
- Relying on the grouped digits shown in the field, or on
value's length cap, as real per-country validation: neither is phone-number-library-accurate (see the warning above) - Setting
hide-country-selectorwithout also settingcountry(ordefault-country): with no picker UI and no country,valuecan never be built (there's no dial code to build it with), and typed digits go nowhere
Live testing
Properties
InputPhone <eun-input-phone>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| default-country | string | — | The country applied when the field connects, when country isn't already set |
| hide-country-selector | boolean | false | Hides the leading country selector part entirely. Country can still be set programmatically with no picker shown |
| hide-dial-code | boolean | false | Hides the dial code prefix inside the phone field |
| country-search-placeholder | string | 'Search a country…' | The placeholder of the search field inside the country dropdown |
| no-results-label | string | 'No results' | Text displayed in the dropdown when no country matches the search |
| aria-label-country-selector | string | 'Select a country' | The accessible label of the leading country selector button |
| country | string | — | The selected country, as a two letter country code. No default is forced, so value stays empty until a country is selected. The country selector button shows a generic globe glyph while unset |
| placeholder | string | — | The placeholder shown when the field is empty. Defaults to a generic formatted digit pattern, not a real per-country example, when unset |
| locale | string | undefined | — | The locale used to display each country's name in the dropdown (via `Intl.DisplayNames`) ; defaults to English. |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| variant | 'outline' | 'fill' | 'underline' | 'outline' | The visual variant to apply to the field |
| label | string | — | The title label of the field, rendered through the label component |
| instructions | string | — | Instructions displayed below the label |
| name | string | — | The field name |
| value | string | — | The field value, as an E.164 string, such as +33612345678 |
| defaultValue | string | — | The default value applied when the field connects, and restored on form reset, as an E.164 string |
| disabled | boolean | false | Whether the field is disabled |
| readonly | boolean | false | Whether the field is read only |
| required | boolean | false | Whether the field is required |
| hideError | boolean | false | Whether the errors are hidden |
| validators | Array<Validators<string>> | — | The list of validation rules applied to the field value |
Slots
| Name | Description |
|---|---|
| (default) | An optional list of option elements restricting which countries appear in the dropdown, such as an option with value FR for France. When omitted, every built-in country is available |
Events
| Name | Type | Description |
|---|---|---|
| eunchange | ChangeEvent | Fired on every value change |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --field-background | Sets the background color of the field |
| --field-border-color | Sets the border color of the field |
| --field-border-color-hover | Sets the border color on hover |
| --field-border-color-focus | Sets the border color on focus |
| --field-border-radius | Sets the corner radius of the field |
| --field-padding | Sets the padding of the field |
| --field-text-color | Sets the text color of the field |
| --field-placeholder-color | Sets the color of the placeholder |
| --field-icon-color | Sets the color of the country selector button |
| --field-error-color | Sets the color of the error message |
| --field-hint-color | Sets the color of the hint message |
| --field-focus-outline-color | Sets the color of the focus outline |
| --field-listbox-max-height | Sets the maximum height of the country dropdown panel, past which it scrolls |
| --field-listbox-transition-duration | Sets the duration of the dropdown panel's open and close height transition, disabled entirely under reduced motion settings |
| --eun-viewport-inset-top | Reserves space, such as a page's sticky header height, that the dropdown panel keeps clear of at the top of the viewport. Set on the root or an ancestor rather than on the field itself |
| --eun-viewport-inset-bottom | The same as eun-viewport-inset-top, reserved at the bottom of the viewport, such as for a sticky footer |
Countries
Every country the dropdown offers by default (i.e. with no restricting
<option> children, see "Country dropdown" in the Examples tab), with the
flag/name shown in its own row and the +<dial code> prefix plus maximum
national number length (221 total) this
component derives from it. Generated straight from
input-phone.type.ts's own table, so it always matches what actually ships.
| Flag | Country | ISO | Dial code | Max digits |
|---|---|---|---|---|
| 🇦🇫 | Afghanistan | AF |
+93 | 9 |
| 🇦🇱 | Albania | AL |
+355 | 9 |
| 🇩🇿 | Algeria | DZ |
+213 | 9 |
| 🇦🇸 | American Samoa | AS |
+1684 | 7 |
| 🇦🇩 | Andorra | AD |
+376 | 6 |
| 🇦🇴 | Angola | AO |
+244 | 9 |
| 🇦🇮 | Anguilla | AI |
+1264 | 7 |
| 🇦🇬 | Antigua & Barbuda | AG |
+1268 | 7 |
| 🇦🇷 | Argentina | AR |
+54 | 10 |
| 🇦🇲 | Armenia | AM |
+374 | 8 |
| 🇦🇼 | Aruba | AW |
+297 | 7 |
| 🇦🇺 | Australia | AU |
+61 | 9 |
| 🇦🇹 | Austria | AT |
+43 | 11 |
| 🇦🇿 | Azerbaijan | AZ |
+994 | 9 |
| 🇧🇸 | Bahamas | BS |
+1242 | 7 |
| 🇧🇭 | Bahrain | BH |
+973 | 8 |
| 🇧🇩 | Bangladesh | BD |
+880 | 10 |
| 🇧🇧 | Barbados | BB |
+1246 | 7 |
| 🇧🇾 | Belarus | BY |
+375 | 9 |
| 🇧🇪 | Belgium | BE |
+32 | 9 |
| 🇧🇿 | Belize | BZ |
+501 | 7 |
| 🇧🇯 | Benin | BJ |
+229 | 8 |
| 🇧🇹 | Bhutan | BT |
+975 | 8 |
| 🇧🇴 | Bolivia | BO |
+591 | 8 |
| 🇧🇦 | Bosnia & Herzegovina | BA |
+387 | 8 |
| 🇧🇼 | Botswana | BW |
+267 | 8 |
| 🇧🇷 | Brazil | BR |
+55 | 11 |
| 🇻🇬 | British Virgin Islands | VG |
+1284 | 7 |
| 🇧🇳 | Brunei | BN |
+673 | 7 |
| 🇧🇬 | Bulgaria | BG |
+359 | 9 |
| 🇧🇫 | Burkina Faso | BF |
+226 | 8 |
| 🇧🇮 | Burundi | BI |
+257 | 8 |
| 🇰🇭 | Cambodia | KH |
+855 | 9 |
| 🇨🇲 | Cameroon | CM |
+237 | 9 |
| 🇨🇦 | Canada | CA |
+1 | 10 |
| 🇨🇻 | Cape Verde | CV |
+238 | 7 |
| 🇰🇾 | Cayman Islands | KY |
+1345 | 7 |
| 🇨🇫 | Central African Republic | CF |
+236 | 8 |
| 🇹🇩 | Chad | TD |
+235 | 8 |
| 🇨🇱 | Chile | CL |
+56 | 9 |
| 🇨🇳 | China | CN |
+86 | 11 |
| 🇨🇴 | Colombia | CO |
+57 | 10 |
| 🇰🇲 | Comoros | KM |
+269 | 7 |
| 🇨🇬 | Congo - Brazzaville | CG |
+242 | 9 |
| 🇨🇩 | Congo - Kinshasa | CD |
+243 | 9 |
| 🇨🇰 | Cook Islands | CK |
+682 | 5 |
| 🇨🇷 | Costa Rica | CR |
+506 | 8 |
| 🇨🇮 | Côte d’Ivoire | CI |
+225 | 10 |
| 🇭🇷 | Croatia | HR |
+385 | 9 |
| 🇨🇺 | Cuba | CU |
+53 | 8 |
| 🇨🇾 | Cyprus | CY |
+357 | 8 |
| 🇨🇿 | Czechia | CZ |
+420 | 9 |
| 🇩🇰 | Denmark | DK |
+45 | 8 |
| 🇩🇯 | Djibouti | DJ |
+253 | 8 |
| 🇩🇲 | Dominica | DM |
+1767 | 7 |
| 🇩🇴 | Dominican Republic | DO |
+1809 | 7 |
| 🇪🇨 | Ecuador | EC |
+593 | 9 |
| 🇪🇬 | Egypt | EG |
+20 | 10 |
| 🇸🇻 | El Salvador | SV |
+503 | 8 |
| 🇬🇶 | Equatorial Guinea | GQ |
+240 | 9 |
| 🇪🇷 | Eritrea | ER |
+291 | 7 |
| 🇪🇪 | Estonia | EE |
+372 | 8 |
| 🇸🇿 | Eswatini | SZ |
+268 | 8 |
| 🇪🇹 | Ethiopia | ET |
+251 | 9 |
| 🇫🇴 | Faroe Islands | FO |
+298 | 6 |
| 🇫🇯 | Fiji | FJ |
+679 | 7 |
| 🇫🇮 | Finland | FI |
+358 | 10 |
| 🇫🇷 | France | FR |
+33 | 9 |
| 🇬🇫 | French Guiana | GF |
+594 | 9 |
| 🇵🇫 | French Polynesia | PF |
+689 | 6 |
| 🇬🇦 | Gabon | GA |
+241 | 8 |
| 🇬🇲 | Gambia | GM |
+220 | 7 |
| 🇬🇪 | Georgia | GE |
+995 | 9 |
| 🇩🇪 | Germany | DE |
+49 | 11 |
| 🇬🇭 | Ghana | GH |
+233 | 9 |
| 🇬🇮 | Gibraltar | GI |
+350 | 8 |
| 🇬🇷 | Greece | GR |
+30 | 10 |
| 🇬🇱 | Greenland | GL |
+299 | 6 |
| 🇬🇩 | Grenada | GD |
+1473 | 7 |
| 🇬🇵 | Guadeloupe | GP |
+590 | 9 |
| 🇬🇺 | Guam | GU |
+1671 | 7 |
| 🇬🇹 | Guatemala | GT |
+502 | 8 |
| 🇬🇳 | Guinea | GN |
+224 | 9 |
| 🇬🇼 | Guinea-Bissau | GW |
+245 | 7 |
| 🇬🇾 | Guyana | GY |
+592 | 7 |
| 🇭🇹 | Haiti | HT |
+509 | 8 |
| 🇭🇳 | Honduras | HN |
+504 | 8 |
| 🇭🇰 | Hong Kong SAR China | HK |
+852 | 8 |
| 🇭🇺 | Hungary | HU |
+36 | 9 |
| 🇮🇸 | Iceland | IS |
+354 | 7 |
| 🇮🇳 | India | IN |
+91 | 10 |
| 🇮🇩 | Indonesia | ID |
+62 | 11 |
| 🇮🇷 | Iran | IR |
+98 | 10 |
| 🇮🇶 | Iraq | IQ |
+964 | 10 |
| 🇮🇪 | Ireland | IE |
+353 | 9 |
| 🇮🇱 | Israel | IL |
+972 | 9 |
| 🇮🇹 | Italy | IT |
+39 | 10 |
| 🇯🇲 | Jamaica | JM |
+1876 | 7 |
| 🇯🇵 | Japan | JP |
+81 | 10 |
| 🇯🇴 | Jordan | JO |
+962 | 9 |
| 🇰🇿 | Kazakhstan | KZ |
+7 | 10 |
| 🇰🇪 | Kenya | KE |
+254 | 9 |
| 🇰🇮 | Kiribati | KI |
+686 | 5 |
| 🇰🇼 | Kuwait | KW |
+965 | 8 |
| 🇰🇬 | Kyrgyzstan | KG |
+996 | 9 |
| 🇱🇦 | Laos | LA |
+856 | 9 |
| 🇱🇻 | Latvia | LV |
+371 | 8 |
| 🇱🇧 | Lebanon | LB |
+961 | 8 |
| 🇱🇸 | Lesotho | LS |
+266 | 8 |
| 🇱🇷 | Liberia | LR |
+231 | 8 |
| 🇱🇾 | Libya | LY |
+218 | 9 |
| 🇱🇮 | Liechtenstein | LI |
+423 | 7 |
| 🇱🇹 | Lithuania | LT |
+370 | 8 |
| 🇱🇺 | Luxembourg | LU |
+352 | 9 |
| 🇲🇴 | Macao SAR China | MO |
+853 | 8 |
| 🇲🇬 | Madagascar | MG |
+261 | 9 |
| 🇲🇼 | Malawi | MW |
+265 | 9 |
| 🇲🇾 | Malaysia | MY |
+60 | 9 |
| 🇲🇻 | Maldives | MV |
+960 | 7 |
| 🇲🇱 | Mali | ML |
+223 | 8 |
| 🇲🇹 | Malta | MT |
+356 | 8 |
| 🇲🇭 | Marshall Islands | MH |
+692 | 7 |
| 🇲🇶 | Martinique | MQ |
+596 | 9 |
| 🇲🇷 | Mauritania | MR |
+222 | 8 |
| 🇲🇺 | Mauritius | MU |
+230 | 8 |
| 🇾🇹 | Mayotte | YT |
+262 | 9 |
| 🇲🇽 | Mexico | MX |
+52 | 10 |
| 🇫🇲 | Micronesia | FM |
+691 | 7 |
| 🇲🇩 | Moldova | MD |
+373 | 8 |
| 🇲🇨 | Monaco | MC |
+377 | 8 |
| 🇲🇳 | Mongolia | MN |
+976 | 8 |
| 🇲🇪 | Montenegro | ME |
+382 | 8 |
| 🇲🇦 | Morocco | MA |
+212 | 9 |
| 🇲🇿 | Mozambique | MZ |
+258 | 9 |
| 🇲🇲 | Myanmar (Burma) | MM |
+95 | 9 |
| 🇳🇦 | Namibia | NA |
+264 | 9 |
| 🇳🇷 | Nauru | NR |
+674 | 7 |
| 🇳🇵 | Nepal | NP |
+977 | 10 |
| 🇳🇱 | Netherlands | NL |
+31 | 9 |
| 🇳🇨 | New Caledonia | NC |
+687 | 6 |
| 🇳🇿 | New Zealand | NZ |
+64 | 9 |
| 🇳🇮 | Nicaragua | NI |
+505 | 8 |
| 🇳🇪 | Niger | NE |
+227 | 8 |
| 🇳🇬 | Nigeria | NG |
+234 | 10 |
| 🇳🇺 | Niue | NU |
+683 | 4 |
| 🇰🇵 | North Korea | KP |
+850 | 10 |
| 🇲🇰 | North Macedonia | MK |
+389 | 8 |
| 🇳🇴 | Norway | NO |
+47 | 8 |
| 🇴🇲 | Oman | OM |
+968 | 8 |
| 🇵🇰 | Pakistan | PK |
+92 | 10 |
| 🇵🇼 | Palau | PW |
+680 | 7 |
| 🇵🇸 | Palestinian Territories | PS |
+970 | 9 |
| 🇵🇦 | Panama | PA |
+507 | 8 |
| 🇵🇬 | Papua New Guinea | PG |
+675 | 8 |
| 🇵🇾 | Paraguay | PY |
+595 | 9 |
| 🇵🇪 | Peru | PE |
+51 | 9 |
| 🇵🇭 | Philippines | PH |
+63 | 10 |
| 🇵🇱 | Poland | PL |
+48 | 9 |
| 🇵🇹 | Portugal | PT |
+351 | 9 |
| 🇵🇷 | Puerto Rico | PR |
+1787 | 7 |
| 🇶🇦 | Qatar | QA |
+974 | 8 |
| 🇷🇪 | Réunion | RE |
+262 | 9 |
| 🇷🇴 | Romania | RO |
+40 | 9 |
| 🇷🇺 | Russia | RU |
+7 | 10 |
| 🇷🇼 | Rwanda | RW |
+250 | 9 |
| 🇼🇸 | Samoa | WS |
+685 | 7 |
| 🇸🇲 | San Marino | SM |
+378 | 10 |
| 🇸🇹 | São Tomé & Príncipe | ST |
+239 | 7 |
| 🇸🇦 | Saudi Arabia | SA |
+966 | 9 |
| 🇸🇳 | Senegal | SN |
+221 | 9 |
| 🇷🇸 | Serbia | RS |
+381 | 9 |
| 🇸🇨 | Seychelles | SC |
+248 | 7 |
| 🇸🇱 | Sierra Leone | SL |
+232 | 8 |
| 🇸🇬 | Singapore | SG |
+65 | 8 |
| 🇸🇰 | Slovakia | SK |
+421 | 9 |
| 🇸🇮 | Slovenia | SI |
+386 | 8 |
| 🇸🇧 | Solomon Islands | SB |
+677 | 7 |
| 🇸🇴 | Somalia | SO |
+252 | 8 |
| 🇿🇦 | South Africa | ZA |
+27 | 9 |
| 🇰🇷 | South Korea | KR |
+82 | 10 |
| 🇸🇸 | South Sudan | SS |
+211 | 9 |
| 🇪🇸 | Spain | ES |
+34 | 9 |
| 🇱🇰 | Sri Lanka | LK |
+94 | 9 |
| 🇰🇳 | St. Kitts & Nevis | KN |
+1869 | 7 |
| 🇱🇨 | St. Lucia | LC |
+1758 | 7 |
| 🇵🇲 | St. Pierre & Miquelon | PM |
+508 | 6 |
| 🇻🇨 | St. Vincent & Grenadines | VC |
+1784 | 7 |
| 🇸🇩 | Sudan | SD |
+249 | 9 |
| 🇸🇷 | Suriname | SR |
+597 | 7 |
| 🇸🇪 | Sweden | SE |
+46 | 9 |
| 🇨🇭 | Switzerland | CH |
+41 | 9 |
| 🇸🇾 | Syria | SY |
+963 | 9 |
| 🇹🇼 | Taiwan | TW |
+886 | 9 |
| 🇹🇯 | Tajikistan | TJ |
+992 | 9 |
| 🇹🇿 | Tanzania | TZ |
+255 | 9 |
| 🇹🇭 | Thailand | TH |
+66 | 9 |
| 🇹🇱 | Timor-Leste | TL |
+670 | 8 |
| 🇹🇬 | Togo | TG |
+228 | 8 |
| 🇹🇴 | Tonga | TO |
+676 | 7 |
| 🇹🇹 | Trinidad & Tobago | TT |
+1868 | 7 |
| 🇹🇳 | Tunisia | TN |
+216 | 8 |
| 🇹🇷 | Türkiye | TR |
+90 | 10 |
| 🇹🇲 | Turkmenistan | TM |
+993 | 8 |
| 🇹🇨 | Turks & Caicos Islands | TC |
+1649 | 7 |
| 🇹🇻 | Tuvalu | TV |
+688 | 6 |
| 🇻🇮 | U.S. Virgin Islands | VI |
+1340 | 7 |
| 🇺🇬 | Uganda | UG |
+256 | 9 |
| 🇺🇦 | Ukraine | UA |
+380 | 9 |
| 🇦🇪 | United Arab Emirates | AE |
+971 | 9 |
| 🇬🇧 | United Kingdom | GB |
+44 | 10 |
| 🇺🇸 | United States | US |
+1 | 10 |
| 🇺🇾 | Uruguay | UY |
+598 | 8 |
| 🇺🇿 | Uzbekistan | UZ |
+998 | 9 |
| 🇻🇺 | Vanuatu | VU |
+678 | 7 |
| 🇻🇦 | Vatican City | VA |
+379 | 10 |
| 🇻🇪 | Venezuela | VE |
+58 | 10 |
| 🇻🇳 | Vietnam | VN |
+84 | 10 |
| 🇼🇫 | Wallis & Futuna | WF |
+681 | 6 |
| 🇾🇪 | Yemen | YE |
+967 | 9 |
| 🇿🇲 | Zambia | ZM |
+260 | 9 |
| 🇿🇼 | Zimbabwe | ZW |
+263 | 9 |
Variants
Set variant to switch between the bordered box (outline, default), a
filled background with no border (fill), and a bottom-only border
(underline).
<eun-input-phone
variant="fill"
label="Phone number"
country="FR"
></eun-input-phone>
<eun-input-phone
variant="underline"
label="Phone number"
country="FR"
></eun-input-phone>
Part of the shared vocabulary covered in
Value format
The value emitted through eunchange (and used for form submission) is
an E.164 string, e.g. +33612345678: the country's dial code
followed by the raw digits, with no spaces. The visible field only ever
shows the grouped national digits.
<eun-input-phone
label="Phone number"
country="FR"
value="+33612345678"
></eun-input-phone>
Domestic format (leading 0)
Most numbering plans in this table write their numbers with a leading
trunk 0 domestically (e.g. a French mobile is dialed 06 12 34 56 78
at home, but +33 6 12 34 56 78 abroad). Type it that way and the leading
0 stays visible in the field exactly as typed: it's only excluded from
value (E.164 has no place for it) and from the country's own digit cap,
so it never displaces a real digit off the end to make room for a 0 that
was never part of the significant number to begin with. The default
placeholder reflects the same convention, shown as (0) ahead of the
digits, the parentheses being the common way to signal "domestic-only,
drop it internationally".
<eun-input-phone label="Phone number" country="FR"></eun-input-phone>
Typed: 0612345678 (10 digits, domestic format)
Displayed: 06 12 34 56 78 (leading 0 kept)
value: +33612345678 (leading 0 excluded)
Per-country digit limit
Typing stops accepting new digits once the selected country's own maximum national number length is reached: 9 for France below, so an 11-digit paste only keeps the first 9. Switching country re-trims whatever's already typed to the newly-selected country's own limit, in case it's shorter (try pasting the number below, then switching to Andorra in the dropdown).
<eun-input-phone label="Phone number" country="FR"></eun-input-phone>
Typed: 61234567890 (11 digits)
Kept: 612345678 (9 digits, France's own maximum)
No country selected
country has no forced default: until one is set (by you, default-country,
or through the dropdown), the field still accepts typed digits, but value
stays empty since there's no dial code to build an E.164 string with. The
country-selector button shows a generic globe glyph (🌐) instead of a blank
flag while unset.
<eun-input-phone label="Phone number"></eun-input-phone>
Placeholder
When placeholder isn't set explicitly, the field falls back to a
formatted generic digit pattern prefixed with (0), the conventional
parenthesized trunk prefix that hints a leading 0 is welcome and
domestic-only. It always shows the grouping style typed digits will get,
though this is not a real per-country example number (no
phone-number library is used, so there's no per-country length data to
draw one from). Set placeholder explicitly to override it.
<eun-input-phone
label="Phone number"
country="FR"
placeholder="Your phone number"
></eun-input-phone>
Hiding the dial code indicator
Set hide-dial-code to omit the +<dial code> prefix inside the phone
field. It's useful in tight layouts, or when the dial code is shown
elsewhere.
<eun-input-phone
hide-dial-code
label="Phone number"
country="FR"
></eun-input-phone>
Hiding the country selector
Set hide-country-selector to remove the leading flag/dropdown part
entirely, leaving only the phone field. country can still be set
programmatically (e.g. a fixed-country form): there's simply no picker UI
to change it.
<eun-input-phone
hide-country-selector
label="Phone number"
country="FR"
></eun-input-phone>
Country dropdown
The leading button opens a searchable list of countries (flag, name, dial
code), filtered as you type in its own search field. Like
popover="auto" element: rendered in the browser's top layer and
dismissed automatically on outside click or Escape.
By default, every built-in country is available. Pass
<option value="ISO">Name</option> elements as children to restrict the
dropdown to a specific set, the same convention
default-country sets the initial selection when country isn't
provided explicitly.
<eun-input-phone label="Phone number" default-country="FR">
<option value="FR">France</option>
<option value="BE">Belgium</option>
<option value="CH">Switzerland</option>
<option value="CA">Canada</option>
<option value="US">United States</option>
<option value="GB">United Kingdom</option>
<option value="DE">Germany</option>
</eun-input-phone>
States
Disabled
<eun-input-phone
disabled
label="Phone number"
country="FR"
value="+33612345678"
></eun-input-phone>
Readonly
<eun-input-phone
readonly
label="Phone number"
country="FR"
value="+33612345678"
></eun-input-phone>
Required
<eun-input-phone required label="Phone number" country="FR"></eun-input-phone>
Hint and validation
Same hint/validators/hide-error mechanics as every other field built
on FormMixin. See
<eun-input-phone
label="Phone number"
country="FR"
hint="We'll only text you a verification code."
></eun-input-phone>
Keyboard interactions
| Key | Action |
|---|---|
Tab / Shift+Tab |
Moves focus between the country-selector button, the phone field, and the rest of the page |
Enter / Space, on the country-selector button |
Opens the country dropdown (native <button> behavior) |
ArrowDown / ArrowUp, inside the dropdown's search field |
Moves the highlight to the next/previous matching country |
Enter, inside the dropdown's search field |
Selects the highlighted country, closes the dropdown, and returns focus to the phone field |
| Any character key, inside the dropdown's search field | Filters the country list live |
Escape |
Closes the dropdown (native Popover API light-dismiss) |
Dismissal behavior
The dropdown closes on: Escape, a click outside it (both native Popover
API light-dismiss), picking a country, the browser window itself losing
focus (e.g. Alt-Tabbing to another application, not something the Popover
API handles on its own, since light-dismiss only reacts to clicks and
Escape), and focus leaving the trigger/dropdown entirely (e.g. Tab-ing
past the search field, since light-dismiss doesn't cover keyboard-only focus
changes either). It also closes if the field itself scrolls behind a
reserved --eun-viewport-inset-top/-bottom band while open. See
"Dropdown positioning" below, and
Aria attributes
- The country-selector button carries
aria-haspopup="listbox",aria-expanded(reflecting whether the dropdown is open),aria-controls="listbox", and a configurablearia-label(ariaLabelCountrySelector, "Select a country" by default): its visible content (a flag only) isn't a sufficient accessible name on its own - The dropdown panel itself carries
role="listbox"with a matchingaria-label, and each country row carriesrole="option"/aria-selected="true"\|"false" - The phone field carries
aria-invalid="true"\|"false", kept in sync with the field's validity, andaria-describedby="description"whenever a hint or error message is actually rendered below it - The phone field is associated with its
eun-labelthrough thefor/idrelationship, exactly likeeun-input . Refer to its Accessibility tab for mandatory-field indication and message management, which apply identically here
Dropdown positioning
The dropdown panel's position tracks the whole field (both the
country-selector button and the phone field together) on every scroll and
resize, flipping above it instead of below whenever there isn't enough room
underneath. This uses the exact same AnchorPositionMixin as
--eun-viewport-inset-top/--eun-viewport-inset-bottom (reserved space
for page chrome like a sticky header) and --field-listbox-max-height both
apply here identically. See eun-select's own Accessibility tab
("Dropdown panel: rendering, positioning, and sizing") for the full
explanation.
Disabled vs. readonly
disabled removes both the country-selector button and the phone field
from the tab order, and excludes the value from form submission entirely.
readonly keeps the phone field focusable and its value submitted, but
prevents typing into it and disables the country-selector button, since
there's nothing to change. This communicates "you can't change this
right now", not "this doesn't apply".
Label accessibility
Always set label (or slot content into eun-label's label slot): a
phone field with no accessible name is as disruptive for screen-reader
users as any other unlabeled text field. The country-selector button's own
aria-label (ariaLabelCountrySelector) is separate from this and always
present, since a flag glyph alone never makes an adequate accessible name.
Reference links