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

maskdial

v2.1.3

Published

Modern phone number formatting library with React/Vue components, TypeScript support, Zod/Yup validators, and WCAG 2.2 accessibility. Drop-in replacement for intl-tel-input.

Readme

MaskDial

Modern phone number formatting library with React/Vue components, TypeScript support, Zod/Yup validators, and full WCAG 2.2 accessibility.

version license bundle size typescript react vue accessibility

Features

  • Framework Components - React hooks/components and Vue 3 composables
  • Country Selector - Searchable dropdown with 200+ countries and flag emojis
  • Form Validation - Zod and Yup schemas for easy form integration
  • TypeScript First - Full type definitions and IntelliSense support
  • Accessible - WCAG 2.2 compliant with ARIA, keyboard navigation, screen reader support
  • Powered by libphonenumber-js - Accurate formatting for 200+ countries
  • Smart Cursor - Maintains cursor position during formatting
  • Multiple Adapters - Vanilla JS, jQuery, React, Vue from single package

Installation

npm install maskdial

Framework-Specific Installation

# React
npm install maskdial react react-dom

# Vue 3
npm install maskdial vue

# With Zod validation
npm install maskdial zod

# With Yup validation
npm install maskdial yup

CDN

<!-- unpkg (latest) -->
<script src="https://unpkg.com/maskdial"></script>

<!-- unpkg (pinned version) -->
<script src="https://unpkg.com/[email protected]"></script>

<!-- jsDelivr (latest) -->
<script src="https://cdn.jsdelivr.net/npm/maskdial"></script>

<!-- jsDelivr (pinned version) -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>

Quick Start

React

import { MaskDialInput, CountrySelect } from 'maskdial/react'

function PhoneForm() {
  const [phone, setPhone] = useState('')
  const [country, setCountry] = useState('US')

  return (
    <div>
      <CountrySelect
        value={country}
        onChange={setCountry}
        priorityCountries={['US', 'CA', 'GB']}
        searchable
      />
      <MaskDialInput
        value={phone}
        onChange={(value, data) => {
          setPhone(value)
          console.log('E.164:', data?.e164)
        }}
        country={country}
        showValidation
      />
    </div>
  )
}

Vue 3

<script setup>
import { ref } from 'vue'
import { useMaskDial } from 'maskdial/vue'

const { value, isValid, e164, onInput, inputRef } = useMaskDial({
  country: 'US'
})
</script>

<template>
  <input
    ref="inputRef"
    :value="value"
    @input="onInput"
    type="tel"
  />
  <p v-if="isValid">Valid: {{ e164 }}</p>
</template>

Vanilla JavaScript

import { MaskDial } from 'maskdial'

const mask = new MaskDial('#phone', {
  country: 'US',
  onFormat: (data) => console.log('E.164:', data.e164)
})

jQuery

import 'maskdial/jquery'

$('#phone').maskDial({
  country: 'US',
  onFormat: (data) => console.log('E.164:', data.formatted)
})

React API

useMaskDial Hook

import { useMaskDial } from 'maskdial/react'

function PhoneInput() {
  const {
    value,           // Current formatted value
    setValue,        // Set raw value (will be formatted)
    country,         // Current country code
    setCountry,      // Change country
    isValid,         // Is number valid?
    isPossible,      // Is number possible?
    e164,            // E.164 format (+12025551234)
    national,        // National format (202) 555-1234
    international,   // International format
    digits,          // Digits only
    inputProps,      // Spread on <input>
    inputRef,        // Attach to input for cursor management
    reset            // Reset to initial state
  } = useMaskDial({
    country: 'US',
    initialValue: '',
    onValueChange: (data) => console.log(data),
    onValidationChange: (isValid, isPossible) => console.log(isValid)
  })

  return <input {...inputProps} ref={inputRef} />
}

MaskDialInput Component

import { MaskDialInput, type MaskDialInputRef } from 'maskdial/react'

// Controlled
<MaskDialInput
  value={phone}
  onChange={(value, data) => setPhone(value)}
  country="US"
  showValidation
  validClassName="border-green-500"
  invalidClassName="border-red-500"
/>

// Uncontrolled with ref
const ref = useRef<MaskDialInputRef>(null)
<MaskDialInput ref={ref} country="US" />

// Access via ref
ref.current?.getE164()    // "+12025551234"
ref.current?.isValid()    // true
ref.current?.focus()

CountrySelect Component

import { CountrySelect } from 'maskdial/react'

<CountrySelect
  value={country}
  onChange={setCountry}
  priorityCountries={['US', 'CA', 'GB', 'AU']}
  searchable
  showDialCode
  showFlag
  placeholder="Select country"
  searchPlaceholder="Search..."
/>

Vue 3 API

useMaskDial Composable

<script setup>
import { useMaskDial } from 'maskdial/vue'

const {
  value,           // Ref<string> - formatted value
  country,         // Ref<CountryCode> - current country
  setCountry,      // (country: CountryCode) => void
  isValid,         // Ref<boolean>
  isPossible,      // Ref<boolean>
  e164,            // ComputedRef<string | undefined>
  national,        // ComputedRef<string>
  international,   // ComputedRef<string>
  digits,          // ComputedRef<string>
  onInput,         // Event handler
  inputRef,        // Template ref
  vModel,          // { value, onInput } for v-bind
  reset            // () => void
} = useMaskDial({
  country: 'US',
  modelValue: phone // Optional v-model binding
})
</script>

<template>
  <!-- Option 1: Manual binding -->
  <input
    ref="inputRef"
    :value="value"
    @input="onInput"
    type="tel"
  />

  <!-- Option 2: v-bind shorthand -->
  <input ref="inputRef" v-bind="vModel" type="tel" />
</template>

Form Validation

Zod

import { z } from 'zod'
import { phoneNumber, phoneNumberTransform, optionalPhoneNumber } from 'maskdial/zod'

// Basic validation
const schema = z.object({
  phone: phoneNumber({ country: 'US' })
})

// With transformation to E.164
const schemaE164 = z.object({
  phone: phoneNumberTransform({ country: 'US', format: 'e164' })
})
// Input: "(202) 555-1234" → Output: "+12025551234"

// Optional phone
const schemaOptional = z.object({
  phone: optionalPhoneNumber({ country: 'US' })
})

// Custom error message
const schemaCustom = z.object({
  phone: phoneNumber({
    country: 'US',
    message: 'Please enter a valid US phone number',
    strict: true  // Require valid, not just possible
  })
})

// Refinement for existing schemas
import { isValidPhone } from 'maskdial/zod'

const customSchema = z.object({
  phone: z.string().refine(isValidPhone('US'), {
    message: 'Invalid phone'
  })
})

Yup

import * as yup from 'yup'
import { yupPhone, yupPhoneE164 } from 'maskdial/yup'

// Basic validation
const schema = yup.object({
  phone: yupPhone({ country: 'US' }).required('Phone is required')
})

// With E.164 transformation
const schemaE164 = yup.object({
  phone: yupPhoneE164('US').required()
})

// Using method extension
import 'maskdial/yup'

const schemaMethod = yup.object({
  phone: yup.string().phone({ country: 'US' }).required()
})

// With transformation
const schemaTransform = yup.object({
  phone: yup.string()
    .phone({ country: 'US' })
    .phoneTransform('e164', 'US')
    .required()
})

Vanilla JavaScript API

import { MaskDial, EVENTS } from 'maskdial'

const mask = new MaskDial('#phone', {
  country: 'US',
  autoDetectCountry: true,
  formatAsYouType: true,
  onFormat: (data) => {
    console.log('Formatted:', data.formatted)
    console.log('E.164:', data.e164)
    console.log('Valid:', data.isValid)
  },
  onValidate: (isValid, isPossible) => {
    console.log('Valid:', isValid, 'Possible:', isPossible)
  },
  onCountryChange: (country) => {
    console.log('Country detected:', country)
  }
})

// Methods
mask.getValue()           // "(202) 555-1234"
mask.getE164()            // "+12025551234"
mask.getNational()        // "(202) 555-1234"
mask.getInternational()   // "+1 202 555 1234"
mask.getDigits()          // "2025551234"
mask.isValid()            // true
mask.isPossible()         // true
mask.getCountry()         // "US"
mask.setCountry('GB')     // Change country
mask.destroy()            // Cleanup

// Events
input.addEventListener(EVENTS.FORMAT, (e) => {
  console.log(e.detail)  // FormatData
})
input.addEventListener(EVENTS.VALIDATE, (e) => {
  console.log(e.detail.isValid)
})

jQuery API

import 'maskdial/jquery'

$('#phone').maskDial({
  country: 'US',
  onFormat: (data) => console.log(data.e164)
})

// Methods
$('#phone').maskDial('getValue')
$('#phone').maskDial('getE164')
$('#phone').maskDial('isValid')
$('#phone').maskDial('setCountry', 'GB')
$('#phone').maskDial('destroy')

// Events
$('#phone').on('maskdial:format', (e, data) => {
  console.log(data.formatted)
})
$('#phone').on('maskdial:validate', (e, isValid, isPossible) => {
  console.log('Valid:', isValid)
})

Country Data

import {
  countries,
  getCountryByIso,
  getCountriesByDialCode,
  searchCountries,
  getSortedCountries
} from 'maskdial/countries'

// Get all countries
countries // Country[]

// Find by ISO code
getCountryByIso('US')
// { name: 'United States', iso: 'US', dialCode: '1', flag: '🇺🇸' }

// Find by dial code (returns array - some codes are shared)
getCountriesByDialCode('1')
// [{ name: 'United States', ... }, { name: 'Canada', ... }, ...]

// Search
searchCountries('united')
// [{ name: 'United States', ... }, { name: 'United Kingdom', ... }, ...]

// Get sorted with priority countries first
getSortedCountries(['US', 'CA', 'GB'])
// [US, CA, GB, Afghanistan, Albania, ...]

Accessibility

MaskDial is WCAG 2.2 compliant with:

  • ARIA attributes - Proper labeling and descriptions
  • Keyboard navigation - Full arrow key, home/end, escape support
  • Screen reader support - Live region announcements
  • Focus management - Focus trap in dropdowns

A11y Utilities

import {
  getPhoneInputAria,
  getCountrySelectAria,
  announce,
  createFocusTrap
} from 'maskdial/a11y'

// Generate ARIA attributes for phone input
const aria = getPhoneInputAria({
  inputId: 'phone',
  label: 'Phone number',
  required: true,
  invalid: !isValid,
  errorMessage: 'Invalid phone number',
  helpText: 'Format: (555) 555-5555'
})

<input {...aria.input} />
<span {...aria.label}>Phone number</span>
<span {...aria.error}>Invalid phone number</span>
<span {...aria.help}>Format: (555) 555-5555</span>

// Screen reader announcements
announce('Phone number is valid')
announce('Error: Invalid phone number', 'assertive')

// Focus trap for dropdowns
const trap = createFocusTrap(dropdownElement)
trap.activate()   // Trap focus
trap.deactivate() // Release focus

Options Reference

| Option | Type | Default | Description | |--------|------|---------|-------------| | country | CountryCode | undefined | ISO 3166-1 alpha-2 country code | | autoDetectCountry | boolean | false | Auto-detect country from input | | formatAsYouType | boolean | true | Format during typing | | onFormat | function | undefined | Callback after formatting | | onValidate | function | undefined | Callback on validation change | | onCountryChange | function | undefined | Callback on country change |

FormatData Object

interface FormatData {
  formatted: string           // Display value
  e164: string | undefined    // +12025551234
  national: string            // (202) 555-1234
  international: string       // +1 202 555 1234
  digits: string              // 2025551234
  country: CountryCode | undefined
  countryCallingCode: string | undefined
  isValid: boolean
  isPossible: boolean
  template: string | undefined
}

Browser Support

Modern browsers: Chrome, Firefox, Safari, Edge (last 2 versions)


Migration from v1

Breaking Changes

  • jQuery no longer bundled - import maskdial/jquery separately
  • Plugin renamed: maskedFormatmaskDial
  • Options: isocountry, phoneCode removed (auto-detected)

Before (v1)

$('#phone').maskedFormat({
  iso: 'US',
  phoneCode: '+1',
  prependCode: true
})

After (v2)

import 'maskdial/jquery'

$('#phone').maskDial({
  country: 'US'
})

Development

npm install        # Install dependencies
npm run dev        # Watch mode
npm test           # Run tests
npm run build      # Production build
npm run lint       # Lint code
npm run typecheck  # Type check

License

MIT