A powerful, fully-featured **Angular international phone number input component** with automatic country detection, real-time formatting, validation, and seamless support for both **Reactive & Template-Driven Forms**.
npm install ngx-phoneA powerful, fully-featured Angular international phone number input component with automatic country detection, real-time formatting, validation, and seamless support for both Reactive & Template-Driven Forms.
> π§ Built using libphonenumber-js with enhanced country detection β fully configurable, accessible, and ideal for global forms.
ngx-phone now supports a structured classNames API that allows you to fully customize every internal UI element without breaking functionality or relying on global CSS hacks.
β
Works perfectly with component-level styles
β
No UI or behavioral regressions
β
Backward compatible with existing inputClass, containerClass, etc.
``ts`
config: {
classNames: {
container: 'my-container',
label: 'my-label',
inputGroup: 'my-input-group',
inputContainer: 'my-input-container',
phoneInput: 'my-phone-input',
countryButton: 'my-country-btn',
flagTrigger: 'my-flag-trigger',
dropdown: 'my-dropdown',
searchInput: 'my-search',
countryItem: 'my-country-item',
errorMessage: 'my-error'
}
}
---
- π International input with real-time emoji flags and dial codes
- π₯ Reactive & Template-Driven Forms support with proper ControlValueAccessor
- β
Smart validation (too short, invalid, required, etc.) with custom validator support
- π’ Auto-format as-you-type or on blur with libphonenumber-js
- π Intelligent form value patching with automatic country detection and formatting
- π Flexible patch validation - validate immediately or wait for user interaction
- π Enhanced country selector with:
- Inline or separate button positioning
- Lockable selection
- Preferred, only, or excluded countries
- Dial code country preferences for shared codes
- π Works seamlessly with FormControl, formControlName, and [(ngModel)]+14155552671
- π― Fine-grained validation error display control (blur, focus, live, etc.)
- π§ Auto-detects country code from number input with immediate flag updates
- π Smart phone number parsing - handles various formats automatically:
- International: 1 (704) 843-890
- With country code: (704) 232-323
- National format: 7048837333
- Raw digits: AsYouType
- π οΈ Built-in formatting using & parsePhoneNumber
- π§ͺ Fully standalone (Angular 14+) or usable via NgModules
- β‘ Optimized change detection for smooth user experience
- π Cross-platform emoji flags with smart Windows compatibility detection
---
`bash`
npm install ngx-phone libphonenumber-js world-countries
---
`typescript
import { NgxPhoneModule } from "ngx-phone";
@Component({
standalone: true,
imports: [NgxPhoneModule],
})
export class YourComponent {}
`
`typescript
import { NgModule } from "@angular/core";
import { NgxPhoneModule } from "ngx-phone";
@NgModule({
imports: [NgxPhoneModule],
})
export class YourModule {}
`
---
`html`
`typescript`
form = this.fb.group({
phone: ["", [Validators.required]],
});
`html`
`typescript
// Component
export class EditUserComponent {
phoneConfig: PhoneInputConfig = {
autoFormat: true,
showCountryCodeInInput: true,
validateOnPatch: true, // β
Validate when loading data
markAsTouchedOnPatch: true, // β
Mark as touched immediately
showErrorsOnPatch: true, // β
Show errors right away
defaultCountry: "US",
};
userForm = this.fb.group({
phone: ["", Validators.required],
});
ngOnInit() {
this.userService.getUser(123).subscribe((user) => {
this.userForm.patchValue({
phone: user.phoneNumber, // Any format: "+14155552671", "7048438900", "(704) 843-890"
});
// β
Automatically formats, validates, and shows errors if invalid
});
}
}
`
`html`
---
Input)| Option | Type | Default | Description |
| --------------------------- | -------------------------------- | ------- | --------------------------------------- |
| defaultCountry | string | 'US' | Initial country ISO2 code |preferredCountries
| | string[] | [] | Countries pinned to top of dropdown |onlyCountries
| | string[] | [] | Limit selectable countries |excludeCountries
| | string[] | [] | Exclude specific countries |fallbackCountry
| | string | 'US' | Fallback when detection fails |dialCodeCountryPreference
| | { [dialCode: string]: string } | {} | Map dial code to preferred country ISO2 |autoDetectCountry
| | boolean | false | Use browser locale for initial country |lockCountrySelection
| | boolean | false | Prevent user from changing country |
| Option | Type | Default | Description |
| --------------------------- | ---------------------------- | ---------- | ------------------------------------------------------------------- |
| separateCountrySelector | boolean | false | Show selector as separate button |countrySelectPosition
| | 'before' \| 'after' | 'before' | Position of country selector |flagPosition
| | 'start' \| 'end' \| 'none' | 'start' | Position of inline flag |showFlags
| | boolean | true | Show emoji flags |showDialCode
| | boolean | false | Show dial code next to flag |showCountryCodeInInput
| | boolean | false | Prepend country code in input |clearInputOnCountryChange
| | boolean | false | Clear input when country changes |showInlineDivider
| | boolean | true | Toggle the divider border between the flag and input in inline mode |
| Option | Type | Default | Description |
| ------------------------ | ------------------------------ | ---------------------- | ------------------------------- |
| placeholder | string | 'Enter phone number' | Input placeholder |customPlaceholder
| | (country: Country) => string | β | Dynamic placeholder per country |customPlaceholderStyle
| | { [key: string]: string } | {} | CSS styles for placeholder |autoFocus
| | boolean | false | Auto-focus input on load |
| Option | Type | Default | Description |
| --------------- | --------------------------------- | ------- | ------------------------------------------- |
| label | string | '' | Label text |labelClass
| | string | '' | CSS class for label |showLabel
| | boolean | false | Show label (auto-enabled if label provided) |labelPosition
| | 'top' \| 'floating' \| 'inline' | 'top' | Label positioning |
| Option | Type | Default | Description |
| ------------------- | ----------------------------- | ----------------------- | ------------------------------ |
| searchEnabled | boolean | true | Show search box in dropdown |searchPlaceholder
| | string | 'Search countries...' | Search input placeholder |noResultsText
| | string | 'No countries found' | No results fallback text |dropdownContainer
| | 'body' \| 'parent' | 'parent' | Dropdown attachment |dropdownWidth
| | string | '100%' | Width of country dropdown |dropdownMaxHeight
| | string | '300px' | Max height of dropdown |dropdownPosition
| | 'auto' \| 'top' \| 'bottom' | 'auto' | Dropdown positioning |closeOnSelect
| | boolean | true | Close dropdown after selection |
| Option | Type | Default | Description |
| ------------------- | ----------------------------------------------------------------- | ----------------- | ----------------------------- |
| format | 'INTERNATIONAL' \| 'NATIONAL' \| 'E164' \| 'RFC3966' | 'INTERNATIONAL' | Output format |autoFormat
| | boolean | true | Auto format as-you-type |nationalMode
| | boolean | false | Use national formatting |validateOnChange
| | boolean | true | Validate while typing |validateOnBlur
| | boolean | true | Validate on blur |strictValidation
| | boolean | false | Require both valid & possible |showErrorsOn
| | 'touched' \| 'dirty' \| 'focus' \| 'blur' \| 'always' \| 'live' | 'dirty' | When to show errors |showErrorMessages
| | boolean | true | Show error text automatically |showInvalidBorder
| | boolean | true | Show red border on error |errorMessages
| | Partial | {} | Override error messages |
| Option | Type | Default | Description |
| ---------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
| validateOnPatch | boolean | false | Run validation when value is patched programmatically |markAsTouchedOnPatch
| | boolean | false | Mark control as touched when patching |showErrorsOnPatch
| | boolean | false | Show validation errors immediately on patch |formatOnPatch
| | boolean | true | Apply formatting when patching (respects autoFormat) |emitOnChangeOnPatch
| | boolean | true | Whether to trigger onChange when value is patched programmatically (marks form as dirty) |
> π‘ Tip: Use these options for edit forms where you load existing data and want immediate validation feedback.
| Option | Type | Default | Description |
| ----------------------- | --------------------------- | ------- | ----------------------------------- |
| inputClass | string | '' | CSS class for input element |buttonClass" |
| string | '' | CSS class for flag/selector buttons |
| containerClass | string | '' | CSS class for main container |
| dropdownClass | string | '' | CSS class for dropdown |
| errorClass | string | '' | CSS class for error message |
| customContainerBorder | boolean | false | Use custom container border |
| containerBorderStyle | { [key: string]: string } | {} | Custom border styles |
| Option | Type | Default | Description |
| -------------- | -------------------------------------------------------------------------- | ---------- | ---------------------- |
| valueMode | 'object' \| 'e164' \| 'international' \| 'national' \| 'raw' \| 'string' | 'string' | Output value format |
| customFormat | (value: string, country: Country) => string | β | Custom format function |
---
| Output | Type | Description |
| ------------------ | -------------------------- | -------------------------------------------------- |
| numberChange | PhoneNumberValue \| null | Emits parsed phone number object |
| countryChange | Country | Emits selected country object (including on patch) |
| validationChange | ValidationResult | Emits current validation state |
| focus | void | Emitted when input gains focus |
| blur | void | Emitted when input loses focus |
| enter | void | Emitted on Enter key press |
``typescript
onCountryChange(country: Country) {
console.log('Country changed to:', country.name);
// Triggered both by user selection AND automatic detection on patch
}
onNumberChange(phoneValue: PhoneNumberValue | null) {
if (phoneValue?.isValid) {
console.log('Valid E164:', phoneValue.e164);
this.saveToBackend(phoneValue.e164);
}
}
onValidationChange(result: ValidationResult) {
if (!result.isValid && result.error) {
this.showCustomError(result.error.message);
}
}
`
---
`typescript`
interface PhoneNumberValue {
countryCode?: string; // ISO2 country code (e.g., 'US')
dialCode?: string; // Dial code with + (e.g., '+1')
e164?: string; // E164 format (e.g., '+1234567890')
formatted?: string; // Formatted display (e.g., '+1 234 567 8900')
national?: string; // National format (e.g., '(234) 567-8900')
international?: string; // International format (e.g., '+1 234 567 8900')
rfc3966?: string; // RFC3966 format (e.g., 'tel:+1-234-567-8900')
isValid?: boolean; // Is the number valid?
isPossible?: boolean; // Is the number possibly valid?
type?: NumberType; // Number type (mobile, fixed, etc.)
country?: Country; // Full country object
raw?: string; // Original input value
}
`typescript`
interface ValidationResult {
isValid: boolean;
isPossible?: boolean;
error?: {
type: "REQUIRED" | "INVALID" | "TOO_SHORT" | "TOO_LONG" | "INVALID_COUNTRY" | "NOT_A_NUMBER" | string;
message: string;
};
type?: NumberType;
}
`typescript`
interface Country {
name: string; // Display name (e.g., 'United States')
nativeName?: string; // Native language name
iso2: string; // ISO2 code (e.g., 'US')
iso3?: string; // ISO3 code (e.g., 'USA')
dialCode: string; // Dial code (e.g., '+1')
flag: string; // Emoji flag (e.g., 'πΊπΈ')
flagUrl?: string; // Flag image URL
format?: string; // Phone format pattern
priority?: number; // Sort priority
areaCodes?: string[]; // Area codes for this country
}
---
)| Method | Returns | Description |
| -------------------------- | -------------------------- | --------------------------------------- |
| getValue() | PhoneNumberValue \| null | Get current parsed phone number |clear()
| | void | Clear input and reset state |setCountry(code: string)
| | void | Programmatically select country by ISO2 |formatNumber(style?)
| | string | Format current number in specific style |
`typescript
import { NgxPhoneComponent } from 'ngx-phone';
@ViewChild(NgxPhoneComponent) phone!: NgxPhoneComponent;
submit() {
const phoneValue = this.phone.getValue();
console.log('E164:', phoneValue?.e164);
console.log('Valid:', phoneValue?.isValid);
// Format in different styles
console.log('National:', this.phone.formatNumber('NATIONAL'));
console.log('International:', this.phone.formatNumber('INTERNATIONAL'));
}
clearPhone() {
this.phone.clear();
}
setToUK() {
this.phone.setCountry('GB');
}
`
---
Use standard ISO 3166-1 alpha-2 codes for all country-related configurations:
`typescript`
// Common examples
"US"; // United States
"GB"; // United Kingdom
"CA"; // Canada
"AU"; // Australia
"IN"; // India
"DE"; // Germany
"FR"; // France
"JP"; // Japan
"CN"; // China
"BR"; // Brazil
When multiple countries share the same dial code, specify your preference:
`typescript`
[config]="{
dialCodeCountryPreference: {
'1': 'US', // Prefer US over Canada for +1
'44': 'GB', // Prefer UK over other British territories
'7': 'RU', // Prefer Russia over Kazakhstan for +7
'262': 'RE', // Prefer RΓ©union over Mayotte for +262
'590': 'GP' // Prefer Guadeloupe over other territories
}
}"
This ensures consistent country selection when users type international numbers and when patching values programmatically.
---
For project-specific validation rules beyond standard phone validation, use custom validators:
`typescript
import { PhoneCustomValidator } from "ngx-phone";
// Example: Block numbers starting with specific digits
export const noStartWithZeroValidator: PhoneCustomValidator = (value, country) => {
const digits = value.replace(/\D/g, "");
if (digits.startsWith("0")) {
return {
type: "STARTS_WITH_ZERO",
message: "Phone number cannot start with 0",
};
}
return null; // Valid
};
// Example: Country-specific validation
export const usAreaCodeValidator: PhoneCustomValidator = (value, country) => {
if (country?.iso2 === "US") {
const digits = value.replace(/\D/g, "");
const areaCode = digits.substring(1, 4); // Skip country code
const invalidAreaCodes = ["555", "999"];
if (invalidAreaCodes.includes(areaCode)) {
return {
type: "INVALID_AREA_CODE",
message: Area code ${areaCode} is not allowed,
};
}
}
return null;
};
// Example: Length validation
export const minLengthValidator: PhoneCustomValidator = (value) => {
const digits = value.replace(/\D/g, "");
if (digits.length < 10) {
return {
type: "TOO_SHORT_CUSTOM",
message: "Phone number must be at least 10 digits",
};
}
return null;
};
`
`typescript`
// In component
customValidators = [noStartWithZeroValidator, usAreaCodeValidator];
`html`
You can override error messages in the config:
`typescript`
phoneConfig = {
errorMessages: {
STARTS_WITH_ZERO: "Invalid: Number cannot begin with 0",
INVALID_AREA_CODE: "This area code is restricted",
INVALID: "Please enter a valid phone number",
REQUIRED: "Phone number is required",
},
};
---
| Mode | Behavior |
| ----------- | ------------------------------------- |
| 'touched' | Show errors after user touches field |'dirty'
| | Show errors after user modifies field |'blur'
| | Show errors when field loses focus |'focus'
| | Show errors while field has focus |'live'
| | Show errors in real-time while typing |'always'
| | Always show errors if invalid |
`typescript
onValidationChange(result: ValidationResult) {
if (!result.isValid && result.error) {
console.log('Error type:', result.error.type);
console.log('Error message:', result.error.message);
// Custom handling based on error type
switch (result.error.type) {
case 'REQUIRED':
this.showRequiredFieldHighlight();
break;
case 'INVALID':
this.showInvalidNumberTooltip();
break;
case 'TOO_SHORT':
this.showLengthHint();
break;
}
}
}
`
ngx-phone automatically detects browser emoji flag support and provides robust cross-platform compatibility:
- Native Emoji Flags: Used on browsers that properly render them (Chrome, Safari, mobile browsers)
- Image Fallbacks: Automatically used on Windows and browsers with poor emoji support
- Smart Detection: Canvas-based pixel analysis determines support in real-time
- Zero Configuration: Works automatically without any setup required
The smart detection resolves the common Windows issue where Unicode emoji flags appear as country code letters, ensuring consistent flag display across all platforms and browsers.
---
The component provides several CSS class hooks for custom styling:
`scss
// Main container
.ngx-phone-host {
font-family: "Inter", sans-serif;
}
// Input styling
.phone-input {
font-size: 16px;
padding: 12px;
border-radius: 8px;
}
// Flag button styling
.flag-trigger {
padding: 8px;
&:hover {
background-color: #f3f4f6;
}
}
// Error state
.ngx-phone-host.has-error {
.input-group {
border-color: #ef4444;
}
}
// Custom dropdown styling
.country-dropdown {
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
border-radius: 12px;
}
`
(Recommended)ngx-phone provides a powerful class mapping system that allows you to override
every internal CSS class used by the component β without breaking structure or behavior.
This is the preferred styling approach for design systems and UI libraries.
- β Full control over internal structure
- β No need to rely on deep selectors
- β Safe upgrades (structure remains stable)
- β Works with Tailwind, Bootstrap, custom CSS
---
`ts`
interface PhoneClassMap {
container: string;
label: string;
inputGroup: string;
inputContainer: string;
phoneInput: string;
countryButton: string;
flagTrigger: string;
dropdown: string;
searchInput: string;
countryItem: string;
errorMessage: string;
}
`scss`
:root {
--ngx-phone-border-color: #d1d5db;
--ngx-phone-focus-color: #3b82f6;
--ngx-phone-error-color: #ef4444;
--ngx-phone-border-radius: 8px;
}
`typescript`
styleConfig = {
inputClass: "custom-phone-input",
buttonClass: "custom-flag-button",
containerClass: "phone-container",
errorClass: "phone-error-text",
customContainerBorder: true,
containerBorderStyle: {
border: "2px solid #e5e7eb",
"border-radius": "12px",
"box-shadow": "0 1px 3px rgba(0,0,0,0.1)",
},
customPlaceholderStyle: {
color: "#9ca3af",
"font-style": "italic",
},
};
---
- Keyboard Navigation: Full keyboard support for dropdown and input
- Screen Reader Support: Proper ARIA labels and descriptions
- Focus Management: Logical tab order and focus states
- High Contrast: Respects prefers-contrast: high media queryprefers-reduced-motion
- Reduced Motion: Respects settings
`html
---
π± Responsive Design
The component is fully responsive with mobile-first design:
`scss
// Automatic responsive behavior
@media (max-width: 640px) {
.country-dropdown {
max-width: calc(100vw - 2rem);
margin: 0 1rem;
} .input-group.separate {
flex-direction: column;
gap: 0.5rem;
}
}
`---
π Performance Optimization
$3
- Debounced Validation: 300ms debounce on input validation
- Change Detection: Optimized with
OnPush strategy where applicable
- Virtual Scrolling: For large country lists (coming in v2.0)
- Lazy Loading: Country data loaded on demand
- Memory Management: Proper cleanup of subscriptions and event listeners$3
`typescript
// Use OnPush change detection in parent components
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {}// Limit country lists for better performance
phoneConfig = {
onlyCountries: ["US", "CA", "GB", "AU"], // Faster dropdown
searchEnabled: false, // Disable for small lists
};
`---
π§ͺ Testing
$3
`typescript
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { NgxPhoneModule } from "ngx-phone";describe("PhoneInputComponent", () => {
let component: YourComponent;
let fixture: ComponentFixture;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NgxPhoneModule],
declarations: [YourComponent],
}).compileComponents();
fixture = TestBed.createComponent(YourComponent);
component = fixture.componentInstance;
});
it("should format US number correctly", () => {
const phoneComponent = fixture.debugElement.query(By.directive(NgxPhoneComponent));
phoneComponent.componentInstance.writeValue("+12345678901");
expect(phoneComponent.componentInstance.getValue()?.formatted).toBe("+1 234 567 8901");
});
it("should detect country from patched value", () => {
const phoneComponent = fixture.debugElement.query(By.directive(NgxPhoneComponent));
phoneComponent.componentInstance.writeValue("+441234567890");
expect(phoneComponent.componentInstance.selectedCountry?.iso2).toBe("GB");
});
it("should handle various formats on patch", () => {
const phoneComponent = fixture.debugElement.query(By.directive(NgxPhoneComponent));
// Test different formats
phoneComponent.componentInstance.writeValue("1 (704) 843-890");
expect(phoneComponent.componentInstance.selectedCountry?.iso2).toBe("US");
phoneComponent.componentInstance.writeValue("7048437333");
expect(phoneComponent.componentInstance.phoneValue).toContain("704");
});
});
`$3
`typescript
// Cypress example
cy.get("[data-cy=phone-input]").type("+1234567890").should("have.value", "+1 234 567 8901");cy.get("[data-cy=country-flag]").should("contain.text", "πΊπΈ");
// Test patching
cy.get("@phoneInput").invoke("val", "+441234567890").trigger("input");
cy.get("[data-cy=country-flag]").should("contain.text", "π¬π§");
`---
β FAQ
$3
Check that the country's ISO2 code is included in your
onlyCountries array, or not excluded in excludeCountries.$3
Use
dialCodeCountryPreference to specify which country should be selected by default:`typescript
dialCodeCountryPreference: { '1': 'US' } // Prefer US over Canada for +1
`$3
Yes, use the
errorMessages config and/or custom validators:`typescript
config: {
errorMessages: {
INVALID: 'Please check your phone number',
REQUIRED: 'Phone number is required'
}
}
`$3
Use the patch behavior options:
`typescript
config: {
validateOnPatch: true,
markAsTouchedOnPatch: true,
showErrorsOnPatch: true
}
`$3
Make sure you're not blocking country detection. The component automatically resets manual selection flags when patching. If you have
lockCountrySelection: true, remove it or the flag won't update.$3
The component intelligently handles:
- International:
+14155552671
- With country code: 1 (704) 843-890
- National: (704) 232-323
- Raw digits: 7048437333All formats are automatically normalized, formatted, and validated.
$3
The component works seamlessly with Angular Material form fields:
`html
Phone Number
Invalid phone number
`---
π Contributing
We welcome contributions! Please read our contributing guidelines and submit pull requests to our GitHub repository.
$3
`bash
git clone https://github.com/manishpatidar028/ngx-phone.git
cd ngx-phone
npm install
npm run build
npm run test
`---
π Maintainer
Manish Patidar
π GitHub | LinkedIn
---
π License
MIT License - Free for personal & commercial use
---
β Support
If this library helps your project, please consider:
- β Starring the repo on GitHub
- π Reporting issues and feature requests
- π Contributing to documentation
- π‘ Sharing your use cases and feedback
---
π Links
- GitHub Repository: manishpatidar028/ngx-phone
- NPM Package: ngx-phone
- Demo: Live Demo
- Documentation: Coming Soon π§
- Issues & Support: GitHub Issues
---
π Changelog
$3
- β‘ IMPROVE: Fixed bug related to feature flag
emitOnChangeOnPatch.$3
- β¨ NEW:
emitOnChangeOnPatch feature flag to control form dirty state during programmatic updates.
- β‘ IMPROVE: Fixed several linting warnings and optimized internal component logic.$3
β¨ NEW: classNames styling system (non-breaking)
β‘ IMPROVED: Component-level styling support clarified
$3
- β¨ NEW: Intelligent form value patching with automatic country detection
- β¨ NEW: Flexible patch validation options (
validateOnPatch, markAsTouchedOnPatch, showErrorsOnPatch)
- β¨ NEW: Smart phone number parsing - handles various formats automatically
- β¨ NEW: Automatic flag updates when patching values
- β¨ NEW: Respects showCountryCodeInInput` during patch operations