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-svelte

v1.0.0

Published

🌍 Svelte 5 component and composable for international phone number masking. Powered by @desource/phone-mask with Google libphonenumber sync.

Readme

@desource/phone-mask-svelte

Svelte 5 phone input component with Google's libphonenumber data

npm version bundle size license

Beautiful, accessible, extreme small & tree-shakeable Svelte 5 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
  • 🧩 Two modes β€” Component or composable
  • ⚑ Optimized β€” Tree-shaking and code splitting

πŸ“¦ Installation

npm install @desource/phone-mask-svelte
# or
yarn add @desource/phone-mask-svelte
# or
pnpm add @desource/phone-mask-svelte

πŸš€ Quick Start

Importing

Component mode:

import { PhoneInput } from '@desource/phone-mask-svelte';
import '@desource/phone-mask-svelte/assets/lib.css'; // Import styles

Composable mode:

import { usePhoneMask } from '@desource/phone-mask-svelte';

Component Mode

<script lang="ts">
  import { PhoneInput } from '@desource/phone-mask-svelte';
  import '@desource/phone-mask-svelte/assets/lib.css';

  let phone = $state('');
  let isValid = $state(false);
</script>

<PhoneInput bind:value={phone} country="US" onvalidationchange={(v) => (isValid = v)} />

{#if isValid}
  <p>βœ“ Valid phone number</p>
{/if}

Composable Mode

For custom input implementations:

<script lang="ts">
  import { usePhoneMask } from '@desource/phone-mask-svelte';

  let value = $state('');

  const phoneMask = usePhoneMask({
    value: () => value,
    onChange: (digits) => (value = digits),
    country: () => 'US',
    detect: () => false
  });
</script>

<div>
  <input bind:this={phoneMask.inputRef} type="tel" placeholder="Phone number" />
  <p>Formatted: {phoneMask.fullFormatted}</p>
  <p>Valid: {phoneMask.isComplete ? 'Yes' : 'No'}</p>
  <p>Country: {phoneMask.country.name}</p>
  <button onclick={() => phoneMask.setCountry('GB')}>Use UK</button>
</div>

πŸ“– Component API

Props

Note: The component supports both controlled and bindable modes. Use bind:value for two-way binding or value + onchange for controlled mode.

interface PhoneInputProps {
  // Bindable value (digits only, without country code)
  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

  // Extra CSS class merged onto root element
  class?: string;

  // 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)
  onchange?: (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) => void;

  // Callback when the input is blurred
  onblur?: (event: FocusEvent) => void;

  // Callback when phone number is copied
  oncopy?: (value: string) => void;

  // Callback when input is cleared
  onclear?: () => void;
}

Exposed Methods

Access component methods via bind:this:

<script lang="ts">
  import { PhoneInput } from '@desource/phone-mask-svelte';
  import type { PhoneInputExposed } from '@desource/phone-mask-svelte';

  let phoneInput = $state<PhoneInputExposed | null>(null);
</script>

<PhoneInput bind:this={phoneInput} />
<button onclick={() => phoneInput?.focus()}>Focus</button>
interface PhoneInputExposed {
  focus: () => void; // Focus the input
  blur: () => void; // Blur the input
  clear: () => void; // Clear the input value
  selectCountry: (code: string) => void; // Programmatically select a country by ISO code
  getFullNumber: () => string; // Returns full phone number with country code (e.g. +1234567890)
  getFullFormattedNumber: () => string; // Returns formatted number with country code (e.g. +1 234-567-890)
  getDigits: () => string; // Returns only digits without country code (e.g. 234567890)
  isValid: () => boolean; // Checks if the current phone number is valid
  isComplete: () => boolean; // Alias for isValid()
}

Snippets

<PhoneInput bind:value={phone}>
  {#snippet flag(country)}
    <img src="/flags/{country.code.toLowerCase()}.svg" alt={country.name} />
  {/snippet}

  {#snippet copysvg(copied)}
    {copied ? 'βœ“' : 'πŸ“‹'}
  {/snippet}

  {#snippet clearsvg()}
    βœ•
  {/snippet}

  {#snippet actionsbefore()}
    <button onclick={handleCustomAction}>Custom</button>
  {/snippet}
</PhoneInput>

| Snippet | Props | Description | | --------------- | ------------------------ | ------------------------------------------------- | | flag | MaskFull | Custom flag icon in the country list and selector | | copysvg | boolean (copied state) | Custom copy button icon | | clearsvg | β€” | Custom clear button icon | | actionsbefore | β€” | Content rendered before default action buttons |

🧩 Composable API

Options

Note: The composable uses getter functions for reactive options. Do NOT pass values directly.

interface UsePhoneMaskOptions {
  // Getter returning current digit value (controlled) - REQUIRED
  value: () => string;

  // Callback when the digits value changes - REQUIRED
  onChange: (digits: string) => void;

  // Getter for ISO country code (e.g., 'US', 'DE', 'GB')
  country?: () => string | undefined;

  // Getter for locale string (default: navigator.language)
  locale?: () => string | undefined;

  // Getter for auto-detect flag (default: false)
  detect?: () => boolean | undefined;

  // Callback when the phone changes (full, fullFormatted, digits)
  onPhoneChange?: (phone: PhoneNumber) => void;

  // Country change callback
  onCountryChange?: (country: MaskFull) => void;
}

Return Value

Important: Do NOT destructure the returned object β€” all properties are reactive getters and destructuring breaks reactivity.

interface UsePhoneMaskReturn {
  inputRef: HTMLInputElement | null; // Bind to <input> with bind:this
  digits: string;
  full: string;
  fullFormatted: string;
  isComplete: boolean;
  isEmpty: boolean;
  shouldShowWarn: boolean;
  country: MaskFull;
  setCountry: (countryCode?: string | null) => boolean;
  clear: () => void;
}
<script lang="ts">
  // βœ… CORRECT β€” access as properties
  const phoneMask = usePhoneMask(options);
  phoneMask.digits;

  // ❌ WRONG β€” loses reactivity
  const { digits } = usePhoneMask(options);
</script>

🎨 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 bind:value={phone} theme="dark" />

Or with CSS:

.phone-input[data-theme='dark'] {
  --pi-bg: #1f2937;
  --pi-fg: #f9fafb;
  --pi-border: #374151;
}

πŸ“š Examples

With Validation

<script lang="ts">
  import { PhoneInput } from '@desource/phone-mask-svelte';

  let phone = $state('');
  let isValid = $state(false);

  const errorMessage = $derived(!phone ? '' : isValid ? '' : 'Please enter a valid phone number');
</script>

<div>
  <PhoneInput bind:value={phone} country="US" onvalidationchange={(v) => (isValid = v)} />

  {#if errorMessage}
    <span class="error">{errorMessage}</span>
  {/if}
</div>

Auto-detect Country

<script lang="ts">
  import { PhoneInput } from '@desource/phone-mask-svelte';
  import type { PMaskFull } from '@desource/phone-mask-svelte';

  let phone = $state('');
  let detectedCountry = $state('');

  function handleCountryChange(country: PMaskFull) {
    detectedCountry = country.name;
  }
</script>

<PhoneInput bind:value={phone} detect oncountrychange={handleCountryChange} />

{#if detectedCountry}
  <p>Detected: {detectedCountry}</p>
{/if}

Programmatic Control

<script lang="ts">
  import { PhoneInput } from '@desource/phone-mask-svelte';
  import type { PhoneInputExposed } from '@desource/phone-mask-svelte';

  let phone = $state('');
  let phoneInput = $state<PhoneInputExposed | null>(null);
</script>

<PhoneInput bind:this={phoneInput} bind:value={phone} />

<div>
  <button onclick={() => phoneInput?.focus()}>Focus</button>
  <button onclick={() => phoneInput?.clear()}>Clear</button>
  <button onclick={() => phoneInput?.selectCountry('GB')}>Switch to UK</button>
  <p>Full: {phoneInput?.getFullFormattedNumber()}</p>
  <p>Valid: {phoneInput?.isValid()}</p>
</div>

Multiple Inputs

<script lang="ts">
  import { PhoneInput } from '@desource/phone-mask-svelte';

  let form = $state({ mobile: '', home: '', work: '' });
</script>

<div class="form">
  <label>
    Mobile
    <PhoneInput bind:value={form.mobile} country="US" />
  </label>

  <label>
    Home
    <PhoneInput bind:value={form.home} country="US" />
  </label>

  <label>
    Work
    <PhoneInput bind:value={form.work} country="US" />
  </label>
</div>

Custom Composable Implementation

<script lang="ts">
  import { usePhoneMask } from '@desource/phone-mask-svelte';
  import type { PMaskPhoneNumber } from '@desource/phone-mask-svelte';

  let inputValue = $state('');
  let selectedCountry = $state('US');

  const phoneMask = usePhoneMask({
    value: () => inputValue,
    country: () => selectedCountry,
    detect: () => false,
    onChange: (digits) => {
      inputValue = digits;
    },
    onPhoneChange: (data: PMaskPhoneNumber) => {
      console.log('Phone:', data.fullFormatted);
    }
  });
</script>

<div class="custom-phone">
  <select bind:value={selectedCountry}>
    <option value="US">πŸ‡ΊπŸ‡Έ +1</option>
    <option value="GB">πŸ‡¬πŸ‡§ +44</option>
    <option value="DE">πŸ‡©πŸ‡ͺ +49</option>
  </select>

  <input bind:this={phoneMask.inputRef} type="tel" placeholder="Phone number" />
</div>

<p>Formatted: {phoneMask.fullFormatted}</p>
<p>Valid: {phoneMask.isComplete ? 'Yes' : 'No'}</p>
<p>Country: {phoneMask.country.name}</p>

🎯 Browser Support

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+
  • iOS Safari 14+
  • Chrome Mobile

πŸ“¦ What's Included

@desource/phone-mask-svelte/
β”œβ”€β”€ dist/
β”‚   β”œβ”€β”€ types/                 # TypeScript declaration files
β”‚   β”œβ”€β”€ index.mjs              # ES module bundle
β”‚   β”œβ”€β”€ index.cjs              # CommonJS bundle
β”‚   └── phone-mask-svelte.css  # Component styles
β”œβ”€β”€ README.md                  # This file
└── package.json               # Package manifest

πŸ”— Related

πŸ“„ License

MIT Β© 2026 DeSource Labs

🀝 Contributing

See Contributing Guide