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

@rxova/react-phone-input

v1.0.3

Published

International phone input with no metadata blob: country names from Intl, flags from Unicode, 4 kB of dial codes. Headless, zero-dependency.

Readme

International phone input with no metadata blob. Country names from Intl, flags from Unicode, 4 kB of dial codes. Headless, zero-dependency.

npm install @rxova/react-phone-input

📖 Documentation & live examples → — guides, form recipes, theming, the possible-versus-valid distinction, and migration from another phone library.

  • 10x smaller than the default choice. 4.5 kB brotli against react-phone-number-input's 41.1 kB: names come from Intl.DisplayNames, flags are two regional-indicator letters, and all that is left to ship is the dial-code table.
  • No flag sprite sheet, no icon font. A flag emoji is six bytes of arithmetic.
  • E.164 in and out — one canonical format, never the formatted display text.
  • As-you-type formatting that keeps your caret where you left it, mid-string.
  • Native <select> and native <input type="tel"> — the platform's own picker on mobile, autofill, and keyboard type-ahead for free.
  • Zero runtime dependencies, 4.5 kB brotli, no stylesheet to import.

Basic use

import { useState } from "react";
import { PhoneInput } from "@rxova/react-phone-input";

function Signup() {
  const [phone, setPhone] = useState("");
  return <PhoneInput label="Phone number" value={phone} onChange={setPhone} defaultCountry="GB" />;
}

onChange also receives the details:

import { PhoneInput } from "@rxova/react-phone-input";

function Field() {
  return (
    <PhoneInput
      label="Phone"
      onChange={(value, details) => {
        // value    '+442071234567'
        // details  { e164, country: 'GB', national: '2071234567', possible: true }
        console.log(value, details);
      }}
    />
  );
}

Possible, not valid

The claim this package makes is possibility, and the prop is named possible rather than valid on purpose.

libphonenumber can tell you +1 555 555 5555 is not an assignable US number, because it carries the assignment rules for every carrier block in the world. That is what its metadata buys, and it is genuinely worth its ~40 kB if you are validating a database of numbers.

This tells you the digits are a length that country actually uses, which is what a form field needs to stop a typo — and it costs 4 kB. If you need real validity, validate on the server, where the metadata is free and the answer can be trusted.

import { isPossible, countryByISO2, parsePhone } from "@rxova/react-phone-input";

parsePhone("+442071234567").possible; // true  — 10 digits, which the UK uses
parsePhone("+4420712").possible; // false — no UK number is 5 digits
parsePhone("+9912345678").possible; // false — no such calling code
isPossible(countryByISO2("US"), "4155552671"); // true

Where the data comes from

| What | Source | Cost | | -------------------------------- | -------------------------------------- | --------------------------- | | Country names | Intl.DisplayNames | 0 — every engine ships ICU | | Flags | Two Unicode regional-indicator letters | 0 — six bytes of arithmetic | | Calling codes, lengths, grouping | This package's table | ~4 kB |

That is the whole trick. Bundled locale JSON in this space is mostly names, and the platform has been giving those away since 2019.

import { countryName, flagEmoji } from "@rxova/react-phone-input";

countryName("DE", "en"); // 'Germany'
countryName("DE", "fr"); // 'Allemagne'
flagEmoji("DE"); // '🇩🇪'

On Windows the flag renders as the two letters instead of a flag. That is a legible fallback, not a broken image.

Input the component accepts

Users paste all three of these, so all three work:

| Typed | Read as | | ------------------- | ------------------------------------------------------------------------------------------ | | +44 20 7123 4567 | Explicit international. The calling code decides the country; defaultCountry is ignored. | | 0044 20 7123 4567 | The 00 international prefix, treated identically. | | 020 7123 4567 | National, against the selected country. The trunk 0 is dropped. |

011 is also honoured as the international prefix when the selected country is in the NANP — but only there, because 011x is a legitimate UK area code.

Non-Latin digits are normalised: Arabic-Indic, extended Arabic-Indic and full-width numerals all work, so a user typing on their own keyboard layout is not silently ignored.

The length cap

The field accepts at most 32 characters, and the cap cannot be removed — only moved, with maxLength.

An unbounded phone field is a denial-of-service surface: every keystroke re-parses, re-groups and re-formats the whole contents, and nothing bounds that except the size of what was pasted. So there is always a cap, exactly as there always is in @rxova/react-password-input.

32 is not arbitrary. E.164 caps a number at 15 digits, so the longest text this field can ever produce is 21 characters — a +, the calling code and the grouped digits — which the adversarial suite asserts against the country table itself. 32 is half again as much, which is the room people's own punctuation needs:

| Pasted | Characters | | ---------------------- | ---------- | | +1 (415) 555-2671 | 17 | | +44 (0)20 7123 4567 | 20 | | 0044 (0)20 7123 4567 | 20 |

Nothing written the way people write phone numbers comes close to 32, so no real number is ever truncated — but a pasted file stops at 32 characters instead of reaching the parser.

The cap applies to the text in the box, after grouping, not to the raw keystrokes: maxLength also lands on the <input>, and usePhoneInput enforces it as well, so a programmatic paste and a headless renderer are bounded too. A maxLength below 21 could truncate a number the component had just formatted, so it is refused — the default is used instead and onWarn reports max-length-too-small.

Restricting the list

import { PhoneInput } from "@rxova/react-phone-input";

function Field() {
  return (
    <PhoneInput label="Phone" countries={["GB", "IE", "FR", "DE", "ES"]} defaultCountry="GB" />
  );
}

Order is preserved. Unknown codes are dropped and reported through onWarn; an entirely empty or entirely unknown list falls back to the full table, because a picker with nothing in it is not a usable field.

Forms

With a name, the component emits a hidden input carrying the E.164 value, so a native form posts the canonical number rather than the formatted display text:

import { PhoneInput } from "@rxova/react-phone-input";

function Form() {
  return (
    <form action="/signup" method="post">
      <PhoneInput label="Phone" name="phone" defaultCountry="US" />
      <button type="submit">Sign up</button>
    </form>
  );
}

The visible box shows 415 555 2671; the form receives +14155552671.

React Hook Form, Formik and the rest work through value/onChange as usual, and the ref lands on the <input>, which is what setFocus() and focus-first-error patterns expect.

Styling

There is no stylesheet to import.

| Property | Default | Applies to | | ------------------------- | ---------- | ------------------------------------ | | --rx-phone-gap | 0.375rem | Space between the select and the box | | --rx-phone-select-width | 9rem | Maximum width of the country select |

data-* attributes

These are public API, covered by semver.

| Attribute | On | Meaning | | -------------------------------- | ------------ | -------------------------------- | | data-rx-phone-root | wrapper | Always present | | data-country | wrapper | ISO code of the resolved country | | data-possible | wrapper | The number is a possible length | | data-invalid / data-disabled | wrapper | Mirrors the props | | data-rx-phone-country | <select> | The country picker | | data-rx-phone-input | <input> | The number field | | data-rx-phone-value | hidden input | The E.164 value a form posts |

Headless

usePhoneInput gives you the whole state machine with no markup — including the caret bookkeeping, which is the part worth not rewriting.

import { usePhoneInput } from "@rxova/react-phone-input";

function CustomField() {
  const field = usePhoneInput({ defaultCountry: "GB" });

  return (
    <div onBlur={field.handleBlur}>
      <select
        value={field.country?.iso2}
        onChange={(event) => {
          field.selectCountry(event.target.value);
        }}
      >
        {field.countries.map((country) => (
          <option key={country.iso2} value={country.iso2}>
            {field.flagFor(country.iso2)} {field.nameFor(country.iso2)} +{country.dial}
          </option>
        ))}
      </select>
      <input
        ref={(node) => {
          field.inputRef.current = node;
        }}
        type="tel"
        value={field.text}
        onChange={field.handleInputChange}
      />
    </div>
  );
}

The parsing helpers are exported too — parsePhone, formatPhone, formatNational, isPossible, digitsOnly, lengthsFor — all pure and synchronous.

Diagnostics

onWarn receives a { code, prop, received, message } whenever a prop is rejected or coerced:

import * as Sentry from "@sentry/react";
import { PhoneInput } from "@rxova/react-phone-input";

function Field() {
  return (
    <PhoneInput
      label="Phone"
      onWarn={(warning) => {
        Sentry.captureMessage(warning.message, { level: "warning", extra: { ...warning } });
      }}
    />
  );
}

Codes: unknown-country, unknown-default-country, value-not-e164, value-country-unknown, empty-country-list, locale-invalid, max-length-too-small.

With no handler these go to console.warn. The entire path is stripped from production builds — it sits behind a process.env.NODE_ENV !== 'production' branch, so there is no runtime cost and no console noise in production. The E2E suite asserts this against a real production bundle.

Accessibility

  • A real <input type="tel"> with inputmode="tel" and autocomplete="tel", beside a real <select>. label names it; supply your own <label htmlFor={${id}-input}> when the design wants visible text. Both are native controls on purpose: on a phone the select is the platform's own picker, already searchable and localised, which no custom listbox of 234 options matches.
  • The select carries an accessible name of its own (countryLabel), so it is not announced as an unlabelled combobox.
  • Every option has a name — flag, country name and calling code — so keyboard type-ahead works and nothing is announced blank.
  • The hidden E.164 field is type="hidden", so it is never focusable and never announced; the visible controls are the accessible representation of the value.
  • axe (WCAG 2.1 A/AA) runs over the component in the browser suite and over the whole demo page in Chromium, Firefox and WebKit.

UI-library recipes

The docs contain maintained examples for shadcn/ui, Radix Themes, Material UI, Chakra UI, Mantine and Ant Design. Copy a ready-labelled wrapper with:

npx shadcn@latest add https://rxova.dev/packages/react-inputs/r/phone-field.json

Open the recipes.

Part of rxova

Part of the rxova headless React inputs suite — install the whole set from @rxova/react-inputs, or read the full generated API reference for this package.

Cross-cutting guidance lives on this component's About page: styling and form libraries. Coming from another library? The migration guide maps the props across.

License

MIT © rxova