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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@lbd-sh/lbd-phone-input

v0.0.7

Published

Accessible, framework-agnostic phone input with country code selector and formatting utilities.

Readme

lbd-phone-input

Ultra-flexible, framework-agnostic phone inputs with accessible country selectors, smart formatting, geo-aware defaults, translations, and backend-friendly payloads.

lbd-phone-input is maintained and proudly sponsored by Transfeero, the premium airport transfer platform.


Table of contents


Overview

lbd-phone-input ships as a zero-dependency TypeScript module. It embraces progressive enhancement: a single call transforms any <input type="tel"> into a fully accessible phone widget.


Installation

npm install @lbd-sh/lbd-phone-input
# or
yarn add @lbd-sh/lbd-phone-input
# or
pnpm add @lbd-sh/lbd-phone-input

Requires Node 18+ for local tooling and modern browsers (ES2020).


60-second quick start

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Phone form</title>
    <link rel="stylesheet" href="/node_modules/lbd-phone-input/dist/styles.css" />
  </head>
  <body>
    <label>
      Phone number
      <input id="phone" type="tel" autocomplete="tel" />
    </label>

    <input type="hidden" id="dial" name="dial_code" />
    <input type="hidden" id="national" name="national_number" />
    <input type="hidden" id="full" name="full_number" />

    <script type="module">
      import { createPhoneInput, detectBrowserCountry } from "@lbd-sh/lbd-phone-input";

      const controller = createPhoneInput("#phone", {
        preferredCountries: ["it", "gb", "us"],
        defaultCountry: "it",
        flagDisplay: "sprite",
        theme: "auto",
        nationalMode: true,
        bindings: {
          dialCode: "#dial",
          nationalNumber: "#national",
          combined: "#full"
        }
      });

      detectBrowserCountry().then((iso2) => iso2 && controller.setCountry(iso2));

      document.querySelector("#phone").addEventListener("phone-change", ({ detail }) => {
        console.log("Current state", detail);
      });
    </script>
  </body>
</html>

Why choose lbd-phone-input?

  • Modern UX: Emoji or sprite flags, smart masking, and polished dark/light themes out-of-the-box.
  • Accessible by design: ARIA labels, keyboard navigation, and screen-reader hints are all baked in.
  • Global ready: Realistic placeholders plus translations for the ten most-spoken languages.
  • Geo-smart: Automatically selects the user’s country using browser language hints.
  • Backend friendly: Exposes dial code, national number, formatted value, and E.164 string simultaneously.
  • Framework agnostic: Vanilla TypeScript API works with React, Vue, Svelte, Angular, Bootstrap, Tailwind, or plain HTML.
  • Split-input helper: Easily separate dial-code and phone fields without losing validation logic.

Feature tour

Flag rendering

Choose between emoji flags, a retina PNG sprite, or hide flags completely.

createPhoneInput("#emoji", { flagDisplay: "emoji" });
createPhoneInput("#sprite", { flagDisplay: "sprite" });
createPhoneInput("#minimal", { flagDisplay: "none" });

CDN-ready standalone build

The build step emits a ready-to-host bundle under dist/cdn/ for CDNJS (or any static CDN):

  • dist/cdn/lbd-phone-input.esm.js
  • dist/cdn/lbd-phone-input.cjs
  • dist/cdn/lbd-phone-input.css
  • dist/cdn/assets/ (sprite assets)

Publish those files to CDNJS to offer a standalone script include alongside the npm package.

Adaptive themes

  • theme: "auto" picks up the user’s system preference (light/dark).
  • setTheme("light" | "dark" | "auto") switches themes at runtime.
  • CSS variables (--lbd-bg, --lbd-input-bg, --lbd-text-color, etc.) make it trivial to match any design system.

Realistic placeholders & masking

Every country ships with a plausible example (347 12 12 456 for Italy). Placeholders automatically update when the selection changes, and the input is masked in national mode.

Language support

Built-in translations for English, Italian, Spanish, French, German, Portuguese, Russian, Chinese (Simplified), Japanese, and Arabic. Override or extend strings with translations.

Geo-aware defaults

Call detectBrowserCountry() to pre-select the correct dial code based on browser settings. No external service required.

Split inputs

Use createSplitPhoneInput to keep dial code and national number in two separate fields while sharing formatting, validation, and events.

createSplitPhoneInput({
  dialCode: "#billing-dial",
  nationalNumber: "#billing-phone",
  combined: "#billing-phone-e164"
}, {
  preferredCountries: ["us", "ca"],
  flagDisplay: "sprite"
});

Usage patterns & recipes

Plain HTML

<input id="support-phone" type="tel" class="form-control" />
<script type="module">
  import { createPhoneInput } from "lbd-phone-input";

  createPhoneInput("#support-phone", {
    theme: "dark",
    preferredCountries: ["us", "ca", "mx"],
    autoFormat: true
  });
</script>

Auto-detect & national mode

const controller = createPhoneInput("#shipping-phone", {
  nationalMode: true,
  smartPlaceholder: true
});

detectBrowserCountry().then((iso2) => iso2 && controller.setCountry(iso2));

Split inputs with validation

const split = createSplitPhoneInput({
  dialCode: "#checkout-dial",
  nationalNumber: "#checkout-phone",
  combined: "#checkout-e164"
}, {
  nationalMode: true,
  closeDropdownOnSelection: false
});

document.querySelector("#checkout-phone").addEventListener("phone-change", ({ detail }) => {
  document.querySelector("#error").hidden = detail.isValid;
});

Bulk initialization

import { createPhoneInputs } from "@lbd-sh/lbd-phone-input";

createPhoneInputs('input[data-phone="true"]', {
  flagDisplay: "none",
  autoFormat: false,
  preferredCountries: ["de", "fr", "it"]
});

Inline formatting helper

const { format } = createPhoneInput("#callback-phone");
console.log(format("02079460958")); // "+44 20 7946 0958"

Configuration reference

| Option | Type | Default | Notes | | --- | --- | --- | --- | | countries | Array<CountryDefinition \| Country> | built-in dataset | Supply custom data; flags generated automatically when omitted. | | preferredCountries | string[] | ["it","us","gb","fr","de"] | ISO alpha-2 codes pinned to the top of the list. | | defaultCountry | string | "it" | Initial ISO selection. | | autoFormat | boolean | true | Apply national masks while typing. | | nationalMode | boolean | false | Keep value in national format instead of international E.164. | | smartPlaceholder | boolean | true | Realistic example for the current country. | | searchPlaceholder | string | language dependent | Overrides the translated search placeholder. | | dropdownPlaceholder | string | language dependent | Text shown above the country list. | | ariaLabelSelector | string | language dependent | Accessible label for the flag button. | | language | string | auto (from navigator.language) | Force a specific translation locale. | | translations | Partial<PhoneInputTranslations> | {} | Override individual strings. | | flagDisplay | "emoji" \| "sprite" \| "none" | "emoji" | Flag display mode. | | flagSpriteUrl | string | built-in | Provide your own sprite. | | flagSpriteRetinaUrl | string | built-in | High-DPI sprite. | | theme | "auto" \| "light" \| "dark" | "auto" | Theme mode. | | closeDropdownOnSelection | boolean | true | Keep dropdown open if false. | | bindings | SubmissionBindings | undefined | Sync values into external inputs. | | value | PhoneInputInitialValue | undefined | Prefill dial code / national / combined value. | | onChange | (state) => void | undefined | Subscribe to changes. |


Events & payloads

Each change emits phone-change from the original <input>.

input.addEventListener("phone-change", ({ detail }) => {
  console.log(detail);
});

Sample payload:

{
  "dialCode": "+39",
  "nationalNumber": "3471212456",
  "formattedValue": "+39 347 12 12 456",
  "e164": "+393471212456",
  "country": { "iso2": "it", "name": "Italy", "...": "..." },
  "isValid": true,
  "theme": "dark"
}

Translations

Supported languages: English, Italian, Spanish, French, German, Portuguese, Russian, Chinese (zh), Japanese (ja), Arabic (ar).

createPhoneInput("#support-it", { language: "it" });

createPhoneInput("#custom", {
  translations: {
    searchPlaceholder: "Buscar teléfono",
    dropdownPlaceholder: "Selecciona un destino",
    ariaLabelSelector: "Selecciona el prefijo"
  }
});

Need another locale? Provide overrides for all the fields in PhoneInputTranslations.


Styling & design tokens

| Variable | Purpose | | --- | --- | | --lbd-bg | Widget background color | | --lbd-text-color | Primary text color | | --lbd-muted-color | Secondary text | | --lbd-border-color | Input border | | --lbd-border-radius | Rounded corners | | --lbd-focus-ring | Focus outline shadow | | --lbd-input-bg | Visible input background | | --lbd-input-placeholder | Placeholder color | | --lbd-search-bg / --lbd-search-border | Dropdown search field styling | | --lbd-option-hover | Option hover color | | --lbd-flag-sprite-url / --lbd-flag-sprite-2x-url | Sprite references | | --lbd-font-size | Base font size for the widget | | --lbd-input-padding-y / --lbd-input-padding-x | Input vertical / horizontal padding | | --lbd-selector-padding-x | Country selector horizontal padding | | --lbd-selector-gap | Space between flag and dial code |

.lbd-phone-input {
  --lbd-border-radius: 16px;
  --lbd-border-color: rgba(15, 23, 42, 0.12);
  --lbd-bg: #f8fafc;
  --lbd-text-color: #0f172a;
  --lbd-input-padding-y: 1rem; /* keeps both selector and text field vertically aligned */
}

Combine them with Tailwind or Bootstrap utility classes to match existing form themes.


Framework integration

Plug the controller into any framework. Below you’ll find reference snippets for React, Vue, Angular, and Svelte; other UI libraries follow the same pattern—create the controller when the component mounts and destroy it when it unmounts.

React

import { useEffect, useRef } from 'react';
import { createPhoneInput, detectBrowserCountry, type PhoneInputController } from 'lbd-phone-input';
import 'lbd-phone-input/dist/styles.css';

export function PhoneField() {
  const ref = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (!ref.current) return;
    const controller: PhoneInputController = createPhoneInput(ref.current, { theme: 'auto' });
    detectBrowserCountry().then((iso2) => iso2 && controller.setCountry(iso2));
    return () => controller.destroy();
  }, []);

  return <input ref={ref} type="tel" className="form-control" />;
}

Vue 3 (Composition API)

import { onMounted, onBeforeUnmount, ref } from 'vue';
import { createPhoneInput, type PhoneInputController } from 'lbd-phone-input';
import 'lbd-phone-input/dist/styles.css';

export default {
  setup() {
    const el = ref<HTMLInputElement | null>(null);
    let controller: PhoneInputController | null = null;

    onMounted(() => {
      if (el.value) {
        controller = createPhoneInput(el.value, { theme: 'dark' });
      }
    });

    onBeforeUnmount(() => controller?.destroy());

    return { el };
  }
};

Angular

// phone-input.directive.ts
import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core';
import { createPhoneInput, PhoneInputController } from 'lbd-phone-input';
import 'lbd-phone-input/dist/styles.css';

@Directive({ selector: '[appPhoneInput]' })
export class PhoneInputDirective implements OnInit, OnDestroy {
  private controller?: PhoneInputController;

  constructor(private host: ElementRef<HTMLInputElement>) {}

  ngOnInit() {
    this.controller = createPhoneInput(this.host.nativeElement, { theme: 'auto' });
  }

  ngOnDestroy() {
    this.controller?.destroy();
  }
}
<!-- template -->
<input type="tel" appPhoneInput />

Svelte

<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { createPhoneInput, type PhoneInputController } from 'lbd-phone-input';
  import 'lbd-phone-input/dist/styles.css';

  let input: HTMLInputElement;
  let controller: PhoneInputController;

  onMount(() => {
    controller = createPhoneInput(input, { theme: 'dark' });
  });

  onDestroy(() => controller?.destroy());
</script>

<input bind:this={input} type="tel" class="border rounded px-3 py-2" />
import { onMounted, onBeforeUnmount, ref } from "vue";
import { createPhoneInput, type PhoneInputController } from "@lbd-sh/lbd-phone-input";
import "lbd-phone-input/dist/styles.css";

export default {
  setup() {
    const el = ref<HTMLInputElement | null>(null);
    let controller: PhoneInputController | null = null;

    onMounted(() => {
      if (el.value) {
        controller = createPhoneInput(el.value, { theme: "dark" });
      }
    });

    onBeforeUnmount(() => controller?.destroy());

    return { el };
  }
};

With Tailwind/Bootstrap

<div class="form-floating">
  <input id="bootstrap-phone" type="tel" class="form-control" />
  <label for="bootstrap-phone">Emergency contact</label>
</div>

<script type="module">
  import { createPhoneInput } from "lbd-phone-input";
  createPhoneInput("#bootstrap-phone", { theme: "light", flagDisplay: "sprite" });
</script>

Accessibility & keyboard support

  • Tab focuses the dial selector, Enter opens the list.
  • Arrow keys navigate countries; Enter selects; Esc closes the dropdown.
  • Screen readers announce the currently selected country and dial code.
  • The dropdown search field supports typing without losing focus.


FAQ

Can I use my own country dataset?
Yes. Pass an array of CountryDefinition objects; flags will be generated automatically when flag isn’t provided.

How do I validate the number before submission?
Use the phone-change event payload. detail.isValid checks for basic length; combine with external libraries if you require advanced telecom validation.

Does it work server-side?
Initialization requires window. If rendering on the server, hydrate the component in a useEffect/onMounted hook.

What about RTL languages?
Set language: "ar" (or override via translations). The dropdown inherits document direction; customize with CSS if needed.


Sponsor & license

Created with ❤️ by Transfeero and friends.
Offered by LBD Srl · www.lbdsh.com

This project is released under the MIT License.