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

eu-phone-input

v0.1.3

Published

React phone input component for EU, UK, Norway, and Switzerland numbers with TypeScript, formatting, and validation

Readme

eu-phone-input

A React phone number input component covering all 27 EU member states plus UK, Norway, and Switzerland (30 countries). Works in Vite, Next.js, Remix, Astro with React, and other React apps. Formats numbers as you type, validates length, and handles international input.

npm downloads license


Features

  • 🇪🇺 All 27 EU member states + UK, Norway, Switzerland (30 countries total)
  • 🏳️ Real flag images (no emoji rendering issues on Windows)
  • ✍️ Format as you type — 0831234567083 123 4567
  • 📋 Smart international input — +353831234567 or 00353831234567 switches country and strips the calling code automatically
  • 🔍 Country search + keyboard navigation in the dropdown
  • 🌙 Dark mode out of the box
  • 🎨 Fully themeable with CSS variables
  • 🪝 Headless hook (usePhoneInput) for custom markup
  • 📦 Zero runtime dependencies (React peer dep only)
  • ⚛️ Works with Vite, Next.js, Remix, Astro with React, and plain React apps
  • TypeScript first

Installation

npm install eu-phone-input

Compatibility

eu-phone-input is a React package, not a Next.js-only package. It supports React 18+ and can be used anywhere your app can import React components and CSS.

The PhoneInput component includes "use client" so it works cleanly in the Next.js App Router. Other React build tools safely ignore that directive.


Quick start

import { PhoneInput } from 'eu-phone-input';
import 'eu-phone-input/style.css';

export default function ContactForm() {
  return (
    <PhoneInput
      defaultCountry="IE"
      showValidation
      onChange={(value, meta) => {
        console.log(meta.e164);      // "+353831234567"
        console.log(meta.valid);     // true
        console.log(meta.national);  // "083 123 4567"
        console.log(meta.country);   // { iso2: "IE", name: "Ireland", ... }
      }}
    />
  );
}

Props

| Prop | Type | Default | Description | |---|---|---|---| | defaultCountry | string | "IE" | ISO2 country code shown on mount | | value | string | — | Controlled input value | | showValidation | boolean | false | Show ✓ / ✗ icon next to input | | onChange | (value, meta) => void | — | Fires on every keystroke | | onValidated | (meta) => void | — | Fires on blur when number is valid | | placeholder | string | country default | Override placeholder text | | disabled | boolean | false | Disable the input | | className | string | — | Extra class added to the wrapper element | | renderFlag | (country) => React.ReactNode | — | Override default flag images | | name | string | — | HTML name attribute on the <input> | | id | string | — | HTML id attribute on the <input> |


The meta object

Both onChange and onValidated receive a PhoneMeta object:

interface PhoneMeta {
  valid: boolean;
  e164: string | null;          // "+353831234567"
  national: string | null;      // "083 123 4567"
  international: string | null; // "+353 83 123 4567"
  country: CountryData | null;
  raw: string;                  // exactly what was in the input
}

Styling

By default, country flags are loaded from https://flagcdn.com. If your app has a strict Content Security Policy or needs to avoid third-party image requests, pass renderFlag to render your own local flag asset, emoji, or text label.

Option 1 — CSS variables (recommended)

Override any variable globally or scoped to one instance:

/* globals.css — applies everywhere */
:root {
  --ipi-border:        #6366f1;
  --ipi-focus-color:   #6366f1;
  --ipi-focus-ring:    rgba(99, 102, 241, 0.2);
  --ipi-radius:        8px;
  --ipi-font-size:     15px;
}
/* Scoped to a single component using className="checkout-phone" */
.checkout-phone {
  --ipi-border:      #d97706;
  --ipi-focus-color: #d97706;
}
<PhoneInput className="checkout-phone" defaultCountry="IE" />

Full list of CSS variables:

| Variable | Default | Controls | |---|---|---| | --ipi-border | #d1d5db | Default border colour | | --ipi-bg | #ffffff | Input background | | --ipi-btn-bg | #f9fafb | Country button background | | --ipi-text | #111827 | Text colour | | --ipi-muted | #6b7280 | Placeholder / secondary text | | --ipi-radius | 6px | Border radius | | --ipi-font-size | 14px | Font size | | --ipi-focus-color | #3b82f6 | Border colour on focus | | --ipi-focus-ring | rgba(59,130,246,.15) | Glow shadow on focus | | --ipi-valid-color | #22c55e | Border colour when valid | | --ipi-valid-ring | rgba(34,197,94,.15) | Glow shadow when valid | | --ipi-invalid-color | #ef4444 | Border colour when invalid | | --ipi-invalid-ring | rgba(239,68,68,.15) | Glow shadow when invalid |


Option 2 — Target .ipi-* classes directly

All internal elements have stable class names you can override in your stylesheet. Import your CSS after eu-phone-input/style.css so specificity works normally:

/* Make the dropdown wider */
.ipi-dropdown { min-width: 320px; }

/* Bigger country button */
.ipi-country-btn { padding: 10px 14px; }

/* Monospace input */
.ipi-input { font-family: monospace; }

Class reference:

| Class | Element | |---|---| | .ipi-wrapper | Outer container | | .ipi-country-btn | Flag + calling code button | | .ipi-flag | Flag <img> | | .ipi-calling-code | +353 text next to flag | | .ipi-chevron | Dropdown arrow | | .ipi-input | Phone number <input> | | .ipi-status | ✓ / ✗ validation icon | | .ipi-dropdown | Country list panel | | .ipi-search-wrap | Search input wrapper | | .ipi-search | Search <input> inside dropdown | | .ipi-dropdown-item | Each country row | | .ipi-selected | Currently selected country row | | .ipi-focused | Keyboard-focused country row | | .ipi-no-results | "No countries found" message |


Option 3 — Headless (bring your own UI)

Use usePhoneInput directly and skip the built-in markup and CSS entirely. This is the right choice if you're using Tailwind or a design system:

import { usePhoneInput } from 'eu-phone-input';
// No style.css import needed

export function MyPhoneInput() {
  const {
    inputValue,
    selectedCountry,
    meta,
    isDropdownOpen,
    filteredCountries,
    focusedIndex,
    search,
    searchInputRef,
    handleInputChange,
    handleCountrySelect,
    handleInputBlur,
    handleSearch,
    handleKeyDown,
    setIsDropdownOpen,
  } = usePhoneInput({ defaultCountry: 'IE' });

  return (
    <div className="flex border rounded-lg" onKeyDown={handleKeyDown}>
      {/* Country picker button */}
      <button
        type="button"
        onClick={() => setIsDropdownOpen(!isDropdownOpen)}
        className="flex items-center gap-2 px-3 border-r"
      >
        {selectedCountry.flag} +{selectedCountry.callingCode}
      </button>

      {/* Dropdown */}
      {isDropdownOpen && (
        <div className="absolute top-full mt-1 bg-white border rounded shadow-lg z-50 w-64">
          <input
            ref={searchInputRef}
            value={search}
            onChange={handleSearch}
            placeholder="Search…"
            className="w-full p-2 border-b outline-none"
          />
          {filteredCountries.map((c, i) => (
            <button
              key={c.iso2}
              onClick={() => handleCountrySelect(c)}
              className={`flex gap-2 w-full px-3 py-2 text-left ${
                i === focusedIndex ? 'bg-blue-50' : 'hover:bg-gray-50'
              }`}
            >
              {c.flag} {c.name}
              <span className="ml-auto text-gray-400">+{c.callingCode}</span>
            </button>
          ))}
        </div>
      )}

      {/* Phone input */}
      <input
        value={inputValue}
        onChange={handleInputChange}
        onBlur={handleInputBlur}
        type="tel"
        className="flex-1 px-3 outline-none"
        placeholder="083 123 4567"
      />

      {meta.valid && <span className="text-green-500 px-2 self-center">✓</span>}
    </div>
  );
}

Utility functions

The core parsing logic is exported for use outside the component — useful for server-side validation or custom flows:

import { parsePhone, toE164, formatNational, digitsOnly, getCountry } from 'eu-phone-input';

parsePhone('0831234567', 'IE');
// { valid: true, e164: '+353831234567', national: '083 123 4567', ... }

parsePhone('+353831234567');
// Same result — calling code auto-detected

toE164('0831234567', getCountry('IE')!);
// "+353831234567"

formatNational('07911123456', 'GB');
// "07911 123456"

digitsOnly('+353 083 123 4567');
// "3530831234567"

Country data

import { COUNTRIES, getCountry, DEFAULT_COUNTRY } from 'eu-phone-input';

getCountry('IE');
// {
//   iso2: "IE",
//   name: "Ireland",
//   callingCode: "353",
//   flag: "🇮🇪",
//   minLength: 8,
//   maxLength: 9,
//   leadingZeroStripped: true
// }

minLength and maxLength are the digit count without the leading zero. leadingZeroStripped: true means numbers in local format start with 0 (e.g. 083...) but that 0 is dropped in E.164 format (+353 83...).

All 27 EU member states:

| Country | ISO2 | Code | |---|---|---| | Austria | AT | +43 | | Belgium | BE | +32 | | Bulgaria | BG | +359 | | Croatia | HR | +385 | | Cyprus | CY | +357 | | Czech Republic | CZ | +420 | | Denmark | DK | +45 | | Estonia | EE | +372 | | Finland | FI | +358 | | France | FR | +33 | | Germany | DE | +49 | | Greece | GR | +30 | | Hungary | HU | +36 | | Ireland | IE | +353 | | Italy | IT | +39 | | Latvia | LV | +371 | | Lithuania | LT | +370 | | Luxembourg | LU | +352 | | Malta | MT | +356 | | Netherlands | NL | +31 | | Poland | PL | +48 | | Portugal | PT | +351 | | Romania | RO | +40 | | Slovakia | SK | +421 | | Slovenia | SI | +386 | | Spain | ES | +34 | | Sweden | SE | +46 |

Plus (non-EU European):

| Country | ISO2 | Code | |---|---|---| | Norway | NO | +47 | | Switzerland | CH | +41 | | United Kingdom | GB | +44 |


Validation behaviour

| Input | defaultCountry | Result | |---|---|---| | +353831234567 | any | ✅ Detected from calling code | | 00353831234567 | any | ✅ Detected from 00 prefix | | 0831234567 | IE | ✅ Local format matched | | 831234567 | IE | ✅ Missing leading zero added | | 353831234567 | IE | ✅ Calling code stripped | | 35699123456 | IE | ❌ Treated as local/ambiguous unless entered as +35699123456 or 0035699123456 | | +3530831234567 | any | ✅ Accepted — extra 0 normalised | | 0831234567 | none | ❌ Ambiguous without hint |

The selected country is used as a hint for local-looking numbers. To auto-switch countries from the phone input, enter the number in international format with + or 00, or pick the country from the dropdown first. Bare calling codes such as 356... are not auto-detected because they can overlap with local numbering.


License

MIT