An Angular telephone input component with country dropdown, flags, and robust validation/formatting.
Wraps intl-tel-input for the UI and libphonenumber-js for parsing/validation. Implements ControlValueAccessor so it plugs into Angular Forms.
Emits E.164 by default (e.g.
+14155550123). SSR‑safe via lazy browser‑only import.
- Country dropdown with flags
- E.164 output (display can be national with
nationalMode) - Reactive & template‑driven Forms support (CVA)
- Built‑in validation using libphonenumber‑js
- Enhanced validation: Detects invalid country codes (like "11", "99") and shows appropriate error states
- SSR‑friendly (no
windowon the server) - Easy theming via CSS variables
- Nice UX options: label/hint/error text, sizes, variants, clear button, autofocus, select-on-focus
- New: Masking & caret-friendly as-you-type formatting (optional)
- New: Format only when valid (formatWhenValid) and lock once valid (lockWhenValid) to prevent extra digits
- Angular 17 – 19
- Node 18 or 20
Library
peerDependenciestarget Angular>=17 <20. Your app can be 17, 18, or 19.
npm i ngxsmk-tel-input intl-tel-input libphonenumber-jsUpdate your app’s angular.json:
Optional override to ensure flags resolve (e.g., Vite/Angular 17+): add to your global styles
.iti__flag { background-image: url("/assets/intl-tel-input/img/flags.png"); }
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
.iti__flag { background-image: url("/assets/intl-tel-input/img/[email protected]"); }
}Restart the dev server after changes.
// app.component.ts
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { JsonPipe } from '@angular/common';
import { NgxsmkTelInputComponent, IntlTelI18n, CountryMap } from 'ngxsmk-tel-input';
@Component({
selector: 'app-root',
standalone: true,
imports: [ReactiveFormsModule, NgxsmkTelInputComponent, JsonPipe],
template: `
<form [formGroup]="fg" style="max-width:420px;display:grid;gap:12px">
<ngxsmk-tel-input
formControlName="phone"
label="Phone"
hint="Include area code"
dir="ltr"
[initialCountry]="'US'"
[preferredCountries]="['US','GB','AU']"
[i18n]="enLabels"
[localizedCountries]="enCountries"
[separateDialCode]="true" <!-- dial code after the flag -->
[formatWhenValid]="'typing'" <!-- live mask only when valid -->
[lockWhenValid]="true" <!-- stop extra digits once valid -->
></ngxsmk-tel-input>
<pre>Value: {{ fg.value | json }}</pre>
</form>
`
})
export class AppComponent {
private readonly fb = inject(FormBuilder);
fg = this.fb.group({ phone: ['', Validators.required] });
// English UI labels (dropdown/search/ARIA)
enLabels: IntlTelI18n = {
selectedCountryAriaLabel: 'Selected country',
countryListAriaLabel: 'Country list',
searchPlaceholder: 'Search country',
zeroSearchResults: 'No results',
noCountrySelected: 'No country selected'
};
// Optional: only override the names you care about
enCountries: CountryMap = {
US: 'United States',
GB: 'United Kingdom',
AU: 'Australia',
CA: 'Canada'
};
}Value semantics: the form control value is E.164 (e.g., +14155550123) when valid, or null when empty/invalid.
<form #f="ngForm">
<ngxsmk-tel-input name="phone" [(ngModel)]="phone"></ngxsmk-tel-input>
</form>
<!-- phone is an E.164 string or null -->You can localize the dropdown/search labels and override country names.
Korean example
<ngxsmk-tel-input
[initialCountry]="'KR'"
[preferredCountries]="['KR','US','JP']"
[i18n]="koLabels"
[localizedCountries]="koCountries">
</ngxsmk-tel-input>
// in component
koLabels = {
selectedCountryAriaLabel: '선택한 국가',
countryListAriaLabel: '국가 목록',
searchPlaceholder: '국가 검색',
zeroSearchResults: '결과 없음',
noCountrySelected: '선택된 국가 없음'
};
koCountries = {
KR: '대한민국',
US: '미국',
JP: '일본',
CN: '중국'
};Arabic + RTL example
<ngxsmk-tel-input
dir="rtl"
label="الهاتف"
hint="اكتب رمز المنطقة"
[initialCountry]="'AE'"
[preferredCountries]="['AE','SA','EG']"
[i18n]="arLabels"
[localizedCountries]="arCountries"
[dropdownAttachToBody]="false" <!-- so popup inherits rtl from host -->
></ngxsmk-tel-input>| Name | Type | Default | Description |
|---|---|---|---|
initialCountry |
CountryCode | 'auto' |
'US' |
Starting country. 'auto' uses geoIp stub (US by default). |
preferredCountries |
CountryCode[] |
['US','GB'] |
Pin these at the top. |
onlyCountries |
CountryCode[] |
— | Limit selectable countries. |
nationalMode |
boolean |
false |
If true, display national format in the input. Value still emits E.164. |
separateDialCode |
boolean |
false |
Show dial code outside the input. |
allowDropdown |
boolean |
true |
Enable/disable dropdown. |
placeholder |
string |
'Enter phone number' |
Input placeholder. |
autocomplete |
string |
'tel' |
Native autocomplete. |
disabled |
boolean |
false |
Disable the control. |
label |
string |
— | Optional floating label text. |
hint |
string |
— | Helper text below the control. |
errorText |
string |
— | Custom error text. |
size |
'sm' | 'md' | 'lg' |
'md' |
Control height/typography. |
variant |
'outline' | 'filled' | 'underline' |
'outline' |
Visual variant. |
showClear |
boolean |
true |
Show a clear (×) button when not empty. |
autoFocus |
boolean |
false |
Focus on init. |
selectOnFocus |
boolean |
false |
Select all text on focus. |
formatOnBlur |
boolean |
true |
Pretty‑print on blur (national if nationalMode). |
showErrorWhenTouched |
boolean |
true |
Show error styles only after blur. |
dropdownAttachToBody |
boolean |
true |
Attach dropdown to <body> (avoids clipping/overflow). |
dropdownZIndex |
number |
2000 |
Z‑index for dropdown panel. |
i18n |
IntlTelI18n |
— | Localize dropdown/search/ARIA labels. |
localizedCountries |
Partial<Record<CountryCode, string>> |
— | Override country display names (ISO-2 keys). |
dir |
'ltr' | 'rtl' |
'ltr' |
Text direction for the control. |
autoPlaceholder |
'off' | 'polite' | 'aggressive' |
'polite' |
Example placeholders. Requires utilsScript unless off. |
utilsScript |
string |
— | Path/URL to utils.js (needed for example placeholders). |
customPlaceholder |
(example: string, country: any) => string |
— | Transform the example placeholder. |
clearAriaLabel |
string |
'Clear phone number' |
ARIA label for the clear button. |
lockWhenValid |
boolean |
true |
Prevent appending extra digits once the number is valid (editing/replacing still allowed). |
CountryCodeis the ISO‑2 uppercase code fromlibphonenumber-js(e.g.US,GB).
| Event | Payload | Description |
|---|---|---|
countryChange |
{ iso2: CountryCode } |
Fired when selected country changes. |
validityChange |
boolean |
Fired when validity flips. |
inputChange |
{ raw: string; e164: string | null; iso2: CountryCode } |
Emitted on every keystroke. |
focus(): voidselectCountry(iso2: CountryCode): void
-
No formatting while invalid. As-you-type masking only starts when the digits form a valid number for the selected country.
-
Sri Lanka / “trunk 0”: a national format may include a leading 0 (e.g., 071…). The emitted E.164 always excludes it (+94 71…)—this is expected.
-
Lock when valid: with lockWhenValid enabled, once the number is valid, appending more digits is blocked (you can still delete/replace).
For rare patterns not covered by libphonenumber-js, the control falls back to raw digits (no forced mask) until it becomes valid.
Override on the element or a parent container:
<ngxsmk-tel-input style="
--tel-border:#cbd5e1;
--tel-ring:#22c55e;
--tel-radius:14px;
--tel-dd-item-hover: rgba(34,197,94,.12);
--tel-dd-z: 3000;
"></ngxsmk-tel-input>Available tokens:
- Input:
--tel-bg,--tel-fg,--tel-border,--tel-border-hover,--tel-ring,--tel-placeholder,--tel-error,--tel-radius,--tel-focus-shadow - Dropdown:
--tel-dd-bg,--tel-dd-border,--tel-dd-shadow,--tel-dd-radius,--tel-dd-item-hover,--tel-dd-search-bg,--tel-dd-z
Dark mode: wrap in a .dark parent — tokens adapt automatically.
<ngxsmk-tel-input formControlName="phone"></ngxsmk-tel-input>
<div class="error" *ngIf="fg.get('phone')?.hasError('required')">Phone is required</div>
<div class="error" *ngIf="fg.get('phone')?.hasError('phoneInvalid')">Please enter a valid phone number</div>
<div class="error" *ngIf="fg.get('phone')?.hasError('phoneInvalidCountryCode')">Invalid country code</div>The component now includes enhanced validation that detects and handles various invalid phone number scenarios:
- Input:
"1123456789"or"99123456789" - Error:
phoneInvalidCountryCode - Reason: "11" and "99" are not valid country codes
- Input:
"+9111023533"(India country code, Delhi area code, but incomplete subscriber number) - Error:
phoneInvalid - Reason: Valid country/area codes but invalid number format
- Input:
"+12025551234"(US),"+442071234567"(UK),"+91112345678"(India) - Status: Valid
- Output: E.164 format string
- When valid → control value = E.164 string
- When invalid/empty → value = null, and validator sets appropriate error type
Need national string instead of E.164? Use
(inputChange)and storeraw/nationalyourself, or adapt the emitter to output national.
- The library lazy‑imports
intl-tel-inputonly in the browser (guards withisPlatformBrowser). - No
window/documentusage on the server path.
This repo is an Angular workspace with a library.
# Build the library
ng build ngxsmk-tel-input
# Option A: use it inside a demo app in the same workspace
ng serve demo
# Option B: install locally via tarball in another app
cd dist/ngxsmk-tel-input && npm pack
# in your other app
npm i ../path-to-workspace/dist/ngxsmk-tel-input/ngxsmk-tel-input-<version>.tgzWorkspace aliasing via
tsconfig.pathsalso works (map"ngxsmk-tel-input": ["dist/ngxsmk-tel-input"]).
UI looks unstyled / bullets in dropdown
Add the CSS and assets in angular.json (see Install). Restart the dev server.
Flags don’t show
Ensure the assets copy exists under /assets/intl-tel-input/img and add the CSS override block above.
TS2307: Cannot find module 'ngxsmk-tel-input'
Build the library first so dist/ngxsmk-tel-input exists. If using workspace aliasing, add a paths entry to the root tsconfig.base.json.
Peer dependency conflict when installing
The lib peers are @angular/* >=17 <20. Upgrade your app or install a compatible version.
Vite/Angular “Failed to resolve import …”
Clear .angular/cache, rebuild the lib, and restart ng serve.
- UI powered by
intl-tel-input - Parsing & validation by
libphonenumber-js
Last updated: 2025-08-29
{ "projects": { "your-app": { "architect": { "build": { "options": { "styles": [ "node_modules/intl-tel-input/build/css/intlTelInput.css" ], "assets": [ { "glob": "**/*", "input": "node_modules/intl-tel-input/build/img", "output": "assets/intl-tel-input/img" } ] } } } } } }