npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, πŸ‘‹, I’m Ryan HefnerΒ  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you πŸ™

Β© 2026 – Pkg Stats / Ryan Hefner

@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.

Readme

@desource/phone-mask-react

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

npm version bundle size license

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 styles

Hook 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 value and onChange props 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 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: 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

πŸ“„ License

MIT Β© 2026 DeSource Labs

🀝 Contributing

See Contributing Guide