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

v0.3.0

Published

πŸ”Œ Vue 3 component & directive for international phone number masking. Powered by @desource/phone-mask with Google libphonenumber sync.

Readme

@desource/phone-mask-vue

Vue 3 phone input component with Google's libphonenumber data

npm version bundle size license

Beautiful, accessible, extreme small & tree-shackable Vue 3 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 directive
  • ⚑ Optimized β€” Tree-shaking and code splitting

πŸ“¦ Installation

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

πŸš€ Quick Start

Importing

import { createApp } from 'vue';
import { PhoneInput, vPhoneMask } from '@desource/phone-mask-vue';
import '@desource/phone-mask-vue/assets/lib.css'; // Import styles (for component mode only)

const app = createApp(App);

app.component('PhoneInput', PhoneInput); // Register component if you need component mode
app.directive('phone-mask', vPhoneMask); // Register directive if you need directive mode
app.mount('#app');

If you need both modes:

import { createApp } from 'vue';
import phoneMask from '@desource/phone-mask-vue';
import '@desource/phone-mask-vue/assets/lib.css'; // Import styles for component mode

const app = createApp(App);

app.use(phoneMask); // Registers both component and directive
app.mount('#app');

Component Mode

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

const phoneDigits = ref('');
const isValid = ref(false);
</script>

<template>
  <PhoneInput v-model="phoneDigits" country="US" theme="light" @validation-change="isValid = $event" />

  <p v-if="isValid">βœ“ Valid phone number</p>
</template>

Directive Mode

For custom styling with automatic formatting:

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

const country = ref('US');
const phone = ref('');
const digits = ref('');

const handleChange = (phone: PMaskPhoneNumber) => {
  phone.value = phone.full;
  digits.value = phone.digits;
};
</script>

<template>
  <div class="phone-wrapper">
    <select v-model="country">
      <option value="US">πŸ‡ΊπŸ‡Έ +1</option>
      <option value="GB">πŸ‡¬πŸ‡§ +44</option>
      <option value="DE">πŸ‡©πŸ‡ͺ +49</option>
    </select>

    <input
      v-phone-mask="{
        country,
        onChange: handleChange
      }"
      placeholder="Phone number"
    />
  </div>

  <p>{{ digits }} digits entered</p>
</template>

πŸ“– Component API

Props

interface PhoneInputProps {
  // v-model binding
  modelValue?: 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
}

Events

interface PhoneInputEvents {
  // v-model update
  'update:modelValue': (value: string) => void;

  // Value changed
  // 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)
  change: (value: PMaskPhoneNumber) => void;

  // Country changed
  'country-change': (country: PMaskFull) => void;

  // Validation state changed
  'validation-change': (isValid: boolean) => void;

  // Input focused
  focus: (event: FocusEvent) => void;

  // Input blurred
  blur: (event: FocusEvent) => void;

  // Copy button clicked
  copy: (value: string) => void;

  // When input is cleared
  clear: () => void;
}

Exposed Methods

interface PhoneInputExpose {
  // Focus the phone input
  focus: () => void;

  // Blur the phone input
  blur: () => void;

  // Clear the phone input
  clear: () => void;

  // Select a country by its ISO 3166-1 alpha-2 code
  selectCountry: (country: CountryKey) => void;

  // Get the full phone number with country code (e.g. +1234567890)
  getFullNumber: () => string;

  // Get the full phone number formatted according to country rules (e.g. +1 234-567-890)
  getFullFormattedNumber: () => string;

  // Get only the digits of the phone number without country code (e.g. 234567890)
  getDigits: () => string;

  // Check if the current phone number is valid
  isValid: () => boolean;

  // Check if the current phone number is complete
  isComplete: () => boolean;
}

Slots

  • actions-before β€” Slot for custom actions before default buttons

  • flag β€” Slot for custom country flag rendering in the country list and country selector

  • copy-svg β€” Slot for custom copy button SVG icon

  • clear-svg β€” Slot for custom clear button SVG icon

Usage with Refs

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

const phoneInputRef = ref<InstanceType<typeof PhoneInput>>();

const focusInput = () => {
  phoneInputRef.value?.focus();
};

const clearInput = () => {
  phoneInputRef.value?.clear();
};
</script>

<template>
  <PhoneInput ref="phoneInputRef" v-model="phone" />
  <button @click="focusInput">Focus</button>
  <button @click="clearInput">Clear</button>
</template>

🎨 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

<template>
  <PhoneInput v-model="phone" theme="dark" />
</template>

Or with CSS:

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

🧩 Directive API

Basic Usage

<template>
  <input v-phone-mask="'US'" />
</template>

With Options

interface PMaskDirectiveOptions {
  // Predefined country ISO code (e.g., 'US', 'DE', 'GB')
  country?: string;

  // Locale for country names (default: navigator.language)
  locale?: string;

  // Auto-detect country from IP/locale (default: false)
  detect?: boolean;

  // Value change callback
  onChange?: (phone: PMaskPhoneNumber) => void;

  // Country change callback
  onCountryChange?: (country: PMaskFull) => void;
}
<template>
  <input
    v-phone-mask="{
      country: 'US',
      locale: 'en',
      onChange: handleChange,
      onCountryChange: handleCountryChange
    }"
  />
</template>

Reactive Country

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

const selectedCountry = ref('US');

const handleChange = (phone: PMaskPhoneNumber) => {
  console.log('Phone:', phone.full, 'Digits:', phone.digits);
};
</script>

<template>
  <select v-model="selectedCountry">
    <option value="US">πŸ‡ΊπŸ‡Έ United States</option>
    <option value="GB">πŸ‡¬πŸ‡§ United Kingdom</option>
  </select>

  <input
    v-phone-mask="{
      country: selectedCountry,
      onChange: handleChange
    }"
  />
</template>

πŸ“š Examples

With Validation

<script setup lang="ts">
import { ref, computed } from 'vue';
import { PhoneInput } from '@desource/phone-mask-vue';

const phone = ref('');
const isValid = ref(false);

const errorMessage = computed(() => {
  if (!phone.value) return '';
  return isValid.value ? '' : 'Please enter a valid phone number';
});
</script>

<template>
  <div>
    <PhoneInput v-model="phone" country="US" @validation-change="isValid = $event" />

    <span v-if="errorMessage" class="error">
      {{ errorMessage }}
    </span>
  </div>
</template>

Auto-detect Country

<script setup lang="ts">
import { ref } from 'vue';
import { PhoneInput, type MaskFull } from '@desource/phone-mask-vue';

const phone = ref('');
const detectedCountry = ref('');

const handleCountryChange = (country: MaskFull) => {
  detectedCountry.value = country.name;
};
</script>

<template>
  <PhoneInput v-model="phone" detect @country-change="handleCountryChange" />

  <p v-if="detectedCountry">Detected: {{ detectedCountry }}</p>
</template>

With Form Libraries

VeeValidate

<script setup lang="ts">
import { useField } from 'vee-validate';
import { PhoneInput } from '@desource/phone-mask-vue';

const { value, errorMessage } = useField('phone', (value) => {
  if (!value) return 'Phone is required';
  // Add custom validation
  return true;
});
</script>

<template>
  <PhoneInput v-model="value" />
  <span v-if="errorMessage">{{ errorMessage }}</span>
</template>

Multiple Inputs

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

const form = reactive({
  mobile: '',
  home: '',
  work: ''
});
</script>

<template>
  <div class="form">
    <label>
      Mobile
      <PhoneInput v-model="form.mobile" country="US" />
    </label>

    <label>
      Home
      <PhoneInput v-model="form.home" country="US" />
    </label>

    <label>
      Work
      <PhoneInput v-model="form.work" country="US" />
    </label>
  </div>
</template>

🎯 Browser Support

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

πŸ“¦ What's Included

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

πŸ”— Related

πŸ“„ License

MIT Β© 2026 DeSource Labs

🀝 Contributing

See Contributing Guide