@desource/phone-mask-react
v1.0.0
Published
π± React component and hook for international phone number input with smart masking. Powered by @desource/phone-mask and Google libphonenumber.
Maintainers
Readme
@desource/phone-mask-react
React phone input component and hook with smart masking and Google libphonenumber data
Beautiful, accessible, extreme small & tree-shakeable React phone input with auto-formatting, country selector, and validation.
β¨ Features
- π¨ Beautiful UI β Modern design with light/dark themes
- π Smart Country Search β Fuzzy matching with keyboard navigation
- π Auto-formatting β As-you-type formatting with smart cursor
- β Validation β Built-in validation with visual feedback
- π Copy Button β One-click copy to clipboard
- π Auto-detection β GeoIP and locale-based detection
- βΏ Accessible β ARIA labels, keyboard navigation
- π± Mobile-friendly β Optimized for touch devices
- π― TypeScript β Full type safety
- πͺ Hook API β For custom input implementations
- β‘ Optimized β Tree-shaking and code splitting
π¦ Installation
npm install @desource/phone-mask-react
# or
yarn add @desource/phone-mask-react
# or
pnpm add @desource/phone-mask-reactπ Quick Start
Importing
Component mode:
import { PhoneInput } from '@desource/phone-mask-react';
import '@desource/phone-mask-react/assets/lib.css'; // Import stylesHook mode:
import { usePhoneMask } from '@desource/phone-mask-react';Component Mode
import { useState } from 'react';
import { PhoneInput } from '@desource/phone-mask-react';
import '@desource/phone-mask-react/assets/lib.css'; // Import styles
function App() {
const [phone, setPhone] = useState('');
const [isValid, setIsValid] = useState(false);
return (
<>
<PhoneInput value={phone} onChange={setPhone} onValidationChange={setIsValid} country="US" />
{isValid && <p>β Valid phone number</p>}
</>
);
}Hook Mode
For custom input implementations:
import { useState } from 'react';
import { usePhoneMask } from '@desource/phone-mask-react';
function CustomPhoneInput() {
const [value, setValue] = useState('');
const { ref, digits, full, fullFormatted, isComplete, country, setCountry } = usePhoneMask({
value,
onChange: setValue,
country: 'US',
detect: true
});
return (
<div>
<input ref={ref} type="tel" placeholder="Phone number" />
<p>Formatted: {fullFormatted}</p>
<p>Valid: {isComplete ? 'Yes' : 'No'}</p>
<p>Country: {country.name}</p>
<button onClick={() => setCountry('GB')}>Use UK</button>
</div>
);
}π Component API
Props
Note: The component requires controlled behavior. Both
valueandonChangeprops are required.
interface PhoneInputProps {
// Controlled value (digits only, without country code) - REQUIRED
value: string;
// Preselected country (ISO 3166-1 alpha-2)
country?: CountryKey;
// Auto-detect country from IP/locale
detect?: boolean; // Default: true
// Locale for country names
locale?: string; // Default: browser language
// Size variant
size?: 'compact' | 'normal' | 'large'; // Default: 'normal'
// Visual theme ("auto" | "light" | "dark")
theme?: 'auto' | 'light' | 'dark'; // Default: 'auto'
// Disabled state
disabled?: boolean; // Default: false
// Readonly state
readonly?: boolean; // Default: false
// Show copy button
showCopy?: boolean; // Default: true
// Show clear button
showClear?: boolean; // Default: false
// Show validation state (borders & outline)
withValidity?: boolean; // Default: true
// Custom search placeholder
searchPlaceholder?: string; // Default: 'Search country or code...'
// Custom no results text
noResultsText?: string; // Default: 'No countries found'
// Custom clear button label
clearButtonLabel?: string; // Default: 'Clear phone number'
// Dropdown menu custom CSS class
dropdownClass?: string;
// Disable default styles
disableDefaultStyles?: boolean; // Default: false
// Callback when the digits value changes - REQUIRED
// Returns only the digits without country code (e.g. '234567890')
onChange: (digits: string) => void;
// Callback when the phone number changes.
// Provides an object with:
// - full: Full phone number with country code (e.g. +1234567890)
// - fullFormatted: Full phone number formatted according to country rules (e.g. +1 234-567-890)
// - digits: Only the digits of the phone number without country code (e.g. 234567890)
onPhoneChange?: (value: PhoneNumber) => void;
// Callback when the selected country changes
onCountryChange?: (country: MaskFull) => void;
// Callback when the validation state changes
onValidationChange?: (isValid: boolean) => void;
// Callback when the input is focused
onFocus?: (event: FocusEvent<HTMLInputElement>) => void;
// Callback when the input is blurred
onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
// Callback when phone number is copied
onCopy?: (value: string) => void;
// Callback when input is cleared
onClear?: () => void;
// Render custom action buttons before default ones
renderActionsBefore?: () => ReactNode;
// Render custom flag icons in the country list and country selector
renderFlag?: (country: MaskFull) => ReactNode;
// Render custom copy button SVG
renderCopySvg?: (copied: boolean) => ReactNode;
// Render custom clear button SVG
renderClearSvg?: () => ReactNode;
}Ref Methods
const phoneInputRef = useRef<PhoneInputRef>(null);
phoneInputRef.current?.focus(); // Focuses the input
phoneInputRef.current?.blur(); // Blurs the input
phoneInputRef.current?.clear(); // Clears the input value
phoneInputRef.current?.selectCountry('GB'); // Programmatically selects a country by ISO code (e.g., 'US', 'DE', 'GB')
phoneInputRef.current?.getFullNumber(); // Returns the full phone number with country code (e.g., +1234567890)
phoneInputRef.current?.getFullFormattedNumber(); // Returns the full phone number formatted according to country rules (e.g., +1 234-567-890)
phoneInputRef.current?.getDigits(); // Returns only the digits of the phone number without country code (e.g., 234567890)
phoneInputRef.current?.isValid(); // Checks if the current phone number is valid
phoneInputRef.current?.isComplete(); // Checks if the current phone number is completeπͺ Hook API
Options
Note: The hook requires controlled behavior. Both
valueandonChangeoptions are required.
interface UsePhoneMaskOptions {
// Controlled value (digits only, without country code) - REQUIRED
value: string;
// Callback when the digits value changes - REQUIRED
onChange: (digits: string) => void;
// Predefined country ISO code (e.g., 'US', 'DE', 'GB')
country?: string;
// Locale for country names (default: navigator.language)
locale?: string;
// Auto-detect country from IP/locale (default: false)
detect?: boolean;
// Callback when the phone changes (full, fullFormatted, digits)
onPhoneChange?: (phone: PhoneNumber) => void;
// Country change callback
onCountryChange?: (country: MaskFull) => void;
}Return Value
interface UsePhoneMaskReturn {
ref: RefObject<HTMLInputElement>;
digits: string;
full: string;
fullFormatted: string;
isComplete: boolean;
isEmpty: boolean;
shouldShowWarn: boolean;
country: MaskFull;
setCountry: (countryCode: string) => void;
clear: () => void;
}π¨ Component Styling
CSS Custom Properties
Customize colors via CSS variables:
.phone-input,
.phone-dropdown {
/* Colors */
--pi-bg: #ffffff;
--pi-fg: #111827;
--pi-muted: #6b7280;
--pi-border: #e5e7eb;
--pi-border-hover: #d1d5db;
--pi-border-focus: #3b82f6;
--pi-focus-ring: 3px solid rgb(59 130 246 / 0.15);
--pi-disabled-bg: #f9fafb;
--pi-disabled-fg: #9ca3af;
/* Sizes */
--pi-font-size: 16px;
--pi-height: 44px;
/* Spacing */
--pi-padding: 12px;
/* Border radius */
--pi-radius: 8px;
/* Shadows */
--pi-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--pi-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -2px rgb(0 0 0 / 0.05);
/* Validation */
--pi-warning: #f59e0b;
--pi-warning-light: #fbbf24;
--pi-success: #10b981;
--pi-focus-ring-warning: 3px solid rgb(245 158 11 / 0.15);
--pi-focus-ring-success: 3px solid rgb(16 185 129 / 0.15);
}Dark Theme
<PhoneInput value={phone} theme="dark" />Or with CSS:
.phone-input[data-theme='dark'] {
--pi-bg: #1f2937;
--pi-fg: #f9fafb;
--pi-border: #374151;
}π Examples
With Validation
import { useState } from 'react';
import { PhoneInput } from '@desource/phone-mask-react';
function Example() {
const [phone, setPhone] = useState('');
const [isValid, setIsValid] = useState(false);
return (
<div>
<PhoneInput value={phone} onChange={setPhone} onValidationChange={setIsValid} />
{isValid && <span>β Valid phone number</span>}
</div>
);
}Auto-detect Country
import { useState } from 'react';
import { PhoneInput } from '@desource/phone-mask-react';
function Example() {
const [phone, setPhone] = useState('');
const [detectedCountry, setDetectedCountry] = useState('');
return (
<>
<PhoneInput
value={phone}
detect
onChange={setPhone}
onCountryChange={(country) => setDetectedCountry(country.name)}
/>
{detectedCountry && <p>Detected: {detectedCountry}</p>}
</>
);
}With Form Libraries
React Hook Form
import { useForm, Controller } from 'react-hook-form';
import { PhoneInput } from '@desource/phone-mask-react';
function Example() {
const { control } = useForm({
defaultValues: { phone: '' }
});
return (
<Controller
name="phone"
control={control}
render={({ field }) => (
<PhoneInput value={field.value} onChange={(digits) => field.onChange(digits)} onBlur={field.onBlur} />
)}
/>
);
}Multiple Inputs
import { useState } from 'react';
import { PhoneInput } from '@desource/phone-mask-react';
function Example() {
const [form, setForm] = useState({ mobile: '', home: '', work: '' });
return (
<div className="form">
<label>
Mobile
<PhoneInput value={form.mobile} onChange={(digits) => setForm({ ...form, mobile: digits })} />
</label>
<label>
Home
<PhoneInput value={form.home} onChange={(digits) => setForm({ ...form, home: digits })} />
</label>
<label>
Work
<PhoneInput value={form.work} onChange={(digits) => setForm({ ...form, work: digits })} />
</label>
</div>
);
}π― Browser Support
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- iOS Safari 14+
- Chrome Mobile
π¦ What's Included
@desource/phone-mask-react/
βββ dist/
β βββ esm # ESM bundle + types
β βββ phone-mask-react.cjs.js # CommonJS bundle
β βββ phone-mask-react.css # Component styles
βββ README.md # This file
βββ package.json # Package manifestπ Related
- @desource/phone-mask β Core library
- @desource/phone-mask-nuxt β Nuxt module
- @desource/phone-mask-vue β Vue 3 bindings
- @desource/phone-mask-svelte β Svelte bindings
π License
MIT Β© 2026 DeSource Labs
