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

@eroyeroy/react-phone-mask

v1.0.0

Published

React phone input toolkit with ready UI, headless hook, country masks, formatting, validation and SSR-friendly API.

Readme

@eroyeroy/react-phone-mask

Production-ready React phone input toolkit with country-aware masks, live formatting, validation metadata, E.164 output, accessible country picker, headless hook and pure utility functions.

The package is intentionally split into three usage layers:

  1. UI component — use PhoneInput when you want a ready styled input.
  2. Headless React hook — use usePhoneInput when you want your own markup or design-system input.
  3. Core utilities — use pure functions when React rendering is not needed.

Features

  • Ready-to-use PhoneInput component
  • Headless usePhoneInput hook for custom inputs
  • Pure formatting, parsing and normalization utilities
  • TypeScript-first public API
  • Country-aware phone masks powered by libphonenumber-js
  • E.164 output for backend APIs
  • isPossible and isValid validation flags
  • Controlled and uncontrolled value/country modes
  • Full country list and custom country subsets
  • Single-country mode without an interactive selector
  • Searchable and keyboard-friendly country picker
  • Optional country list virtualization
  • Custom flag renderer
  • CSS variables and class name overrides
  • SSR-friendly ESM and CJS package output

Installation

npm:

npm install @eroyeroy/react-phone-mask

yarn:

yarn add @eroyeroy/react-phone-mask

pnpm:

pnpm add @eroyeroy/react-phone-mask

bun:

bun add @eroyeroy/react-phone-mask

1. Ready-to-use component

Import the component and the stylesheet once in your app.

import { PhoneInput } from "@eroyeroy/react-phone-mask";
import "@eroyeroy/react-phone-mask/styles.css";

export function Form() {
  return (
    <PhoneInput
      defaultCountry="RU"
      label="Phone number"
      onValueChange={(data) => {
        console.log(data.formattedValue);
        console.log(data.e164);
        console.log(data.isValid);
      }}
    />
  );
}

Controlled component usage

For controlled mode, store the formatted value returned from onValueChange.

import { useState } from "react";

import {
  PhoneInput,
  type PhoneCountryCode,
  type PhoneInputValueChangeData,
} from "@eroyeroy/react-phone-mask";

import "@eroyeroy/react-phone-mask/styles.css";

export function ControlledPhoneField() {
  const [value, setValue] = useState("");
  const [country, setCountry] = useState<PhoneCountryCode>("RU");
  const [phoneData, setPhoneData] =
    useState<PhoneInputValueChangeData | null>(null);

  return (
    <PhoneInput
      value={value}
      country={country}
      label="Phone number"
      error={value && !phoneData?.isPossible ? "Enter a valid phone" : undefined}
      onCountryChange={(nextCountry) => {
        setCountry(nextCountry);
      }}
      onValueChange={(data) => {
        setValue(data.formattedValue);
        setPhoneData(data);
      }}
    />
  );
}

PhoneInput owns the native input onChange handler internally so it can format the value and preserve the caret position. Use onValueChange for form state.

2. Headless usage with your own input

Use usePhoneInput when you want to render your own input, selector, layout or design-system component.

import {
  getPhoneCountries,
  usePhoneInput,
  type PhoneCountryCode,
} from "@eroyeroy/react-phone-mask";

const countries = getPhoneCountries({
  countries: ["RU", "US", "DE", "FR"],
  locale: "en",
});

export function CustomPhoneField() {
  const phone = usePhoneInput({
    defaultCountry: "RU",
    onValueChange: (data) => {
      console.log(data.e164);
    },
  });

  return (
    <div className="phone-field">
      <select
        value={phone.country}
        onChange={(event) => {
          phone.setCountry(event.target.value as PhoneCountryCode);
        }}
      >
        {countries.map((country) => (
          <option key={country.code} value={country.code}>
            {country.name} +{country.callingCode}
          </option>
        ))}
      </select>

      <input
        ref={phone.inputRef}
        {...phone.inputProps}
        className="my-input"
      />
    </div>
  );
}

inputRef is important for full headless behavior: it lets the hook restore the caret after every formatting update. If your design-system input uses a different ref prop, forward phone.inputRef to the underlying native <input>.

Headless API

type UsePhoneInputReturn = {
  value: string;
  country: PhoneCountryCode;
  data: PhoneInputValueChangeData;

  setValue: (value: string) => PhoneInputValueChangeData;
  setCountry: (country: PhoneCountryCode) => PhoneInputValueChangeData;

  inputRef: RefObject<HTMLInputElement | null>;
  inputProps: {
    id?: string;
    name?: string;
    value: string;
    type: "tel";
    inputMode: "tel";
    autoComplete: "tel";
    disabled?: boolean;
    required?: boolean;
    onChange: ChangeEventHandler<HTMLInputElement>;
  };
};

3. Pure core utilities

Use these helpers in validation schemas, submit handlers, server actions, tests or non-React code.

import {
  formatPhoneInput,
  getPhoneInputValueData,
  normalizePhoneDigits,
  parsePhoneInput,
} from "@eroyeroy/react-phone-mask";

const formatted = formatPhoneInput("9991234567", {
  country: "RU",
});

const parsed = parsePhoneInput(formatted, "RU");

const data = getPhoneInputValueData("9991234567", {
  country: "RU",
});

console.log(formatted); // "999 123-45-67"
console.log(parsed.e164); // "+79991234567"
console.log(data.isValid); // boolean
console.log(normalizePhoneDigits("+7 (999) 123-45-67")); // "79991234567"

Returned value metadata

onValueChange, usePhoneInput().data and getPhoneInputValueData() return this shape:

type PhoneInputValueChangeData = {
  country: PhoneCountryCode;
  callingCode: string;
  rawValue: string;
  digits: string;
  nationalNumber: string;
  e164: string | null;
  international: string;
  isPossible: boolean;
  isValid: boolean;
  formattedValue: string;
};

Example:

{
  "country": "RU",
  "callingCode": "7",
  "rawValue": "999 123-45-67",
  "digits": "9991234567",
  "nationalNumber": "9991234567",
  "e164": "+79991234567",
  "international": "+7 999 123 45 67",
  "isPossible": true,
  "isValid": true,
  "formattedValue": "999 123-45-67"
}

Use isPossible for live UI feedback and isValid for stricter submit-time validation.

Country helpers

import {
  getPhoneCountries,
  getPhoneCountry,
  getPhoneCountryCallingCode,
  getPhoneCountryFlag,
  getPhoneCountryName,
  getPhoneExample,
  getPhoneNationalDigitsLimit,
} from "@eroyeroy/react-phone-mask";

const countries = getPhoneCountries({ locale: "ru" });
const ru = getPhoneCountry("RU", "ru");
const placeholder = getPhoneExample("RU");
type PhoneCountry = {
  code: PhoneCountryCode;
  name: string;
  callingCode: string;
  flag: string;
};

Single-country mode

Hide the interactive country selector while keeping the calling code visible.

<PhoneInput
  defaultCountry="RU"
  withCountrySelect={false}
  label="Russian phone"
/>

Custom country list

<PhoneInput
  defaultCountry="US"
  countries={["US", "CA", "GB", "DE", "FR"]}
  label="Phone number"
/>

Virtualized country list

Enable virtualization for large lists or custom flag images.

<PhoneInput
  defaultCountry="RU"
  virtualizeCountryList
  label="Phone number"
/>

Custom flags

import type { PhoneCountry } from "@eroyeroy/react-phone-mask";

function renderFlag(country: PhoneCountry) {
  return (
    <img
      alt=""
      src={`https://flagcdn.com/w40/${country.code.toLowerCase()}.png`}
      width={20}
      height={15}
      loading="lazy"
    />
  );
}

export function App() {
  return (
    <PhoneInput
      defaultCountry="RU"
      renderFlag={renderFlag}
      virtualizeCountryList
    />
  );
}

Styling

Import the default stylesheet:

import "@eroyeroy/react-phone-mask/styles.css";

Customize the component with CSS variables:

.rpm-root {
  --rpm-background: #ffffff;
  --rpm-text-color: #0f172a;
  --rpm-muted-text-color: #64748b;
  --rpm-border-color: #cbd5e1;
  --rpm-border-color-hover: #94a3b8;
  --rpm-border-color-focus: #2563eb;
  --rpm-border-color-error: #dc2626;
  --rpm-disabled-background: #f8fafc;
  --rpm-disabled-text-color: #94a3b8;
  --rpm-radius: 12px;
  --rpm-height: 44px;
  --rpm-font-size: 14px;
  --rpm-label-font-size: 14px;
  --rpm-message-font-size: 13px;
}

Example dark variant:

.my-phone-input {
  --rpm-background: #020617;
  --rpm-text-color: #e5e7eb;
  --rpm-muted-text-color: #94a3b8;
  --rpm-border-color: #334155;
  --rpm-border-color-hover: #475569;
  --rpm-border-color-focus: #60a5fa;
}

Class names

<PhoneInput
  defaultCountry="RU"
  className="phone"
  classNames={{
    label: "phone__label",
    control: "phone__control",
    input: "phone__input",
    helperText: "phone__helper",
    errorText: "phone__error",
    countrySelect: {
      button: "phone__country-button",
      dropdown: "phone__country-dropdown",
      option: "phone__country-option",
    },
  }}
/>

PhoneInput props

| Prop | Type | Default | Description | | --- | --- | --- | --- | | value | string | — | Controlled input value. | | defaultValue | string | "" | Initial value for uncontrolled usage. | | onValueChange | (data) => void | — | Called with formatted value and parsed metadata. | | country | PhoneCountryCode | — | Controlled selected country. | | defaultCountry | PhoneCountryCode | "US" | Initial country for uncontrolled usage. | | onCountryChange | (country, data) => void | — | Called when selected country changes. | | countries | PhoneCountryCode[] | all countries | Optional list of allowed countries. | | locale | string | "en" | Country name locale. | | label | ReactNode | — | Input label. | | helperText | ReactNode | — | Helper text below the input. | | error | ReactNode | — | Error text below the input. | | disabled | boolean | false | Disables the input and country selector. | | required | boolean | false | Marks the field as required. | | placeholder | string | generated | Custom placeholder. | | autoPlaceholder | boolean | true | Generates country-specific placeholder. | | international | boolean | false | Formats value with international calling code. | | withCountrySelect | boolean | true | Shows or hides the country selector. | | clearOnCountryChange | boolean | true | Clears value when country changes. | | limitMaxLength | boolean | true | Limits input length by selected country. | | maxNationalDigits | number | country example length | Overrides national digit limit. | | virtualizeCountryList | boolean | false | Enables dropdown virtualization. | | renderFlag | (country) => ReactNode | — | Custom country flag renderer. | | className | string | — | Root class name. | | classNames | PhoneInputClassNames | — | Internal class name overrides. |

Public exports

import {
  PhoneInput,
  CountrySelect,
  usePhoneInput,
  formatPhoneInput,
  parsePhoneInput,
  getPhoneInputValueData,
  normalizePhoneDigits,
  getPhoneCountries,
  getPhoneCountry,
  getPhoneCountryCallingCode,
  getPhoneCountryFlag,
  getPhoneCountryName,
  getPhoneExample,
  getPhoneNationalDigitsLimit,
} from "@eroyeroy/react-phone-mask";

Browser support

The package targets modern React applications and works with Vite, Next.js, Remix and other bundlers that support ESM/CJS packages.

Peer dependencies

{
  "react": ">=18",
  "react-dom": ">=18"
}

License

MIT