@desource/phone-mask-react

中文文档
2.1.0发布于 2026/11/07MITTypeScriptESM + CJS构建来源已验证近 30 天下载 2万

📱 React component and hook for international phone number input with smart masking. Powered by @desource/phone-mask and Google libphonenumber.

原始使用文档
文档目录29 节
  1. ✨ Features
  2. 📦 Installation
  3. 🚀 Quick Start
  4. Importing
  5. Component Mode
  6. Hook Mode
  7. Send Raw Digits to Backend
  8. Dynamic Mask Updates on Country Change
  9. Multi-tenant: tenantId Default Country + Tenant-specific Va…
  10. 🔤 Display Formatting
  11. 📖 Component API
  12. Props
  13. Ref Methods
  14. 🪝 Hook API
  15. Options
  16. Return Value
  17. 🎨 Component Styling
  18. CSS Custom Properties
  19. Dark Theme
  20. 📚 Examples
  21. With Validation
  22. Auto-detect Country
  23. With Form Libraries
  24. Multiple Inputs
  25. 🎯 Browser Support
  26. 📦 What's Included
  27. 🔗 Related Packages
  28. 📄 License
  29. 🤝 Contributing

@desource/phone-mask-react

React phone input component and hook with smart masking and Google libphonenumber data

npm version code coverage SonarCloud Ask CodeWiki Ask Context7 Ask DeepWiki license

Beautiful, accessible, extreme small & tree-shakeable React phone input with auto-formatting, country selector, and validation.

Before upgrading to 2.0.0, review Migration from 1.7.0 for breaking changes and required updates.

✨ Features

  • 🌐 SSR Support — PhoneInput renders formatted HTML on the server and hydrates in the browser
  • 🎨 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

Pasting a complete international number starting with + detects its country and replaces the whole input. Detection tries an exact mask, then calling code and digit count; shared matches use the calling code's default country. It uses mask data, not allocation prefixes, so an unformatted Canadian +1 number defaults to US. Fragments without a calling code retain the selected country and paste at the caret. This works independently of detect, which controls GeoIP and locale detection.

📦 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 styles

Hook mode:

import { usePhoneMask } from '@desource/phone-mask-react';

Core kit utilities and mask data re-exported from the core package subpaths:

import { getFlagEmoji, formatDigitsWithMap } from '@desource/phone-mask-react/core';

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>
  );
}

Send Raw Digits to Backend

import { useState } from 'react';
import { PhoneInput, type PMaskPhoneNumber } from '@desource/phone-mask-react';

function CheckoutForm() {
  const [digits, setDigits] = useState('');

  const handlePhoneChange = (phone: PMaskPhoneNumber) => {
    const payload = {
      phoneDigits: phone.digits, // unformatted, backend-friendly
      phoneFull: phone.full // optional full number with country code
    };

    setDigits(phone.digits);
    void fetch('/api/profile/phone', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(payload)
    });
  };

  return <PhoneInput value={digits} onChange={setDigits} country="US" onPhoneChange={handlePhoneChange} />;
}

Dynamic Mask Updates on Country Change

import { useMemo, useState } from 'react';
import { usePhoneMask, type PCountryKey } from '@desource/phone-mask-react';

function CountryAwareInput() {
  const [digits, setDigits] = useState('');
  const [country, setCountry] = useState<PCountryKey>('US');

  const {
    ref,
    fullFormatted,
    isComplete,
    setCountry: setMaskCountry
  } = usePhoneMask({
    value: digits,
    onChange: setDigits,
    country
  });

  const onCountrySelect = (next: PCountryKey) => {
    setCountry(next);
    setMaskCountry(next); // update formatter immediately
  };

  const label = useMemo(() => (isComplete ? 'complete' : 'incomplete'), [isComplete]);

  return (
    <div>
      <select value={country} onChange={(e) => onCountrySelect(e.target.value as PCountryKey)}>
        <option value="US">US</option>
        <option value="GB">GB</option>
        <option value="DE">DE</option>
      </select>
      <input ref={ref} type="tel" />
      <p>{fullFormatted}</p>
      <p>{label}</p>
    </div>
  );
}

Multi-tenant: tenantId Default Country + Tenant-specific Validation Rules

import { useMemo, useState } from 'react';
import { usePhoneMask, type PCountryKey } from '@desource/phone-mask-react';

type TenantPolicy = {
  defaultCountry: PCountryKey;
  prefixRule?: RegExp;
};

const TENANT_POLICIES: Record<string, TenantPolicy> = {
  acme: { defaultCountry: 'US', prefixRule: /^(202|303)\d{7}$/ },
  globex: { defaultCountry: 'GB', prefixRule: /^7\d{9}$/ }
};

export function TenantPhoneInput({ tenantId }: { tenantId: string }) {
  const policy = TENANT_POLICIES[tenantId] ?? { defaultCountry: 'US' as const };
  const [digits, setDigits] = useState('');

  const phoneMask = usePhoneMask({
    value: digits,
    onChange: setDigits,
    country: policy.defaultCountry
  });

  const isTenantValid = useMemo(() => {
    if (!phoneMask.isComplete) return false;
    return policy.prefixRule ? policy.prefixRule.test(phoneMask.digits) : true;
  }, [phoneMask.isComplete, phoneMask.digits, policy.prefixRule]);

  return (
    <div>
      <input ref={phoneMask.ref} type="tel" />
      <p>Default country: {policy.defaultCountry}</p>
      <p>Tenant validation: {isTenantValid ? 'pass' : 'fail'}</p>
    </div>
  );
}

🔤 Display Formatting

Call formatPhone(value, country?, options?) directly in JSX. It returns text and requires no CSS.

import { formatPhone } from '@desource/phone-mask-react/core';

function Contact({ phone }: { phone: string }) {
  return <span>{formatPhone(phone, 'US', { code: false, flag: true })}</span>;
}
// '+12345678888' displays as '🇺🇸 234 567 8888'.

Omit country to detect a complete international number beginning with +; pass it for numbers without a calling code or to resolve shared calling codes such as US/CA. The optional PhoneFormatOptions argument accepts code (default true) and flag (default false).

The function works during SSR. Unsupported values remain unchanged; empty, whitespace-only, null, and undefined values return empty text. See the formatting rules for detection limits and examples.

📖 Component API

Props

Note: The component requires controlled behavior. Both value and onChange props are required.

interface PhoneInputProps {
  // Controlled value (digits only, without country code) - REQUIRED
  value: string;

  // Optional id/name applied to the underlying <input> for forms/autofill
  id?: string;
  name?: 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: PMaskPhoneNumber) => 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 value and onChange options 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: PMaskPhoneNumber) => void;

  // Country change callback
  onCountryChange?: (country: MaskFull) => void;
}

Return Value

interface UsePhoneMaskReturn {
  // Ref to attach to your input element
  ref: RefObject<HTMLInputElement | null>;

  // Raw digits without formatting (e.g., "1234567890")
  digits: string;

  // Phone formatter instance
  formatter: FormatterHelpers;

  // Full phone number with country code (e.g., "+11234567890")
  full: string;

  // Full phone number formatted (e.g., "+1 123-456-7890")
  fullFormatted: string;

  // Whether the phone number is complete
  isComplete: boolean;

  // Whether the input is empty
  isEmpty: boolean;

  // Whether to show validation warning
  shouldShowWarn: boolean;

  // Current country data
  country: MaskFull;

  // Change country programmatically
  setCountry: (countryCode: string) => void;

  // Clear the input
  clear: () => void;
}

🎨 Component Styling

CSS Custom Properties

Customize colors via CSS variables:

.desource-phone-input,
.desource-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} onChange={setPhone} theme="dark" />

Or with CSS:

.desource-phone-input.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/
│   ├── index.mjs               # Main ESM entry
│   ├── index.cjs               # Main CommonJS entry
│   ├── core.mjs                # Core kit utilities and mask data subpath
│   ├── core.cjs                # Core kit utilities and mask data CJS subpath
│   ├── phone-mask-react.css    # Component styles
│   └── *.d.ts, *.d.cts         # ESM and CommonJS declarations
├── README.md                   # This file
└── package.json                # Package manifest

📄 License

MIT © 2026 DeSource Labs

🤝 Contributing

See Contributing Guide for development, testing, and publication checks.


Made with ❤️ by DeSource Labs

版本与发布记录

@desource/phone-mask-react 共发布 19 个版本,首次发布于 2026/02/16;过去 12 个月发布了 19 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.1.0latest2026/11/074 天前
2.0.02026/10/3111 天前
1.7.02026/10/101 个月前
1.6.42026/09/261 个月前
1.6.32026/09/082 个月前
1.6.22026/08/242 个月前

关于 @desource/phone-mask-react 的常见问题

@desource/phone-mask-react 是什么?

📱 React component and hook for international phone number input with smart masking. Powered by @desource/phone-mask and Google libphonenumber.

如何安装 @desource/phone-mask-react?

在项目目录运行 npm install @desource/phone-mask-react,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^18.0.0 || ^19.0.0)、react-dom(^18.0.0 || ^19.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@desource/phone-mask-react 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@desource/phone-mask-react 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@desource/phone-mask-react 可以商用吗?

@desource/phone-mask-react 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@desource/phone-mask-react 的下载量有多少?

根据 npm 官方下载统计,@desource/phone-mask-react 在 2026/10/12 – 2026/11/10 这 30 天内被下载 19,627 次,日均约 654 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@desource/phone-mask-react 还在维护吗?

最新版本 2.1.0 发布于 2026/11/07(4 天前);至今共发布 19 个版本,其中过去 12 个月发布了 19 个;首次发布于 2026/02/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。