@desource/phone-mask-nuxt
Nuxt module for phone input with Google's libphonenumber data
Drop-in Nuxt module with auto-imports, SSR support, and zero configuration.
Before upgrading to 2.0.0, review Migration from 1.7.0 for breaking changes and required updates.
✨ Features
- 🎯 Zero Config — Works out of the box
- 🔄 Auto-imports — Component, directive, and types by default
- 🌐 SSR Support —
PhoneInputrenders formatted HTML on the server and hydrates in the browser - 🎨 Styleable — Bring your own styles or use defaults
- 🔧 TypeScript — Fully typed
- ⚡ Optimized — Tree-shaking and code splitting
📦 Installation
npm install @desource/phone-mask-nuxt
# or
yarn add @desource/phone-mask-nuxt
# or
pnpm add @desource/phone-mask-nuxt
🚀 Setup
Add the module to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@desource/phone-mask-nuxt']
});
That's it! The component, directive, and related mask types are now auto-imported. You can additionally enable helper/composable auto-imports via module options.
📖 Usage
Basic Component
<template>
<div>
<PhoneInput v-model="phone" country="US" />
<p>Phone: {{ phone }}</p>
</div>
</template>
<script setup lang="ts">
const phone = ref('');
</script>
With Auto-detection
<template>
<PhoneInput v-model="phone" detect @country-change="onCountryChange" />
</template>
<script setup>
const phone = ref('');
const onCountryChange = (country) => {
console.log('Detected country:', country.name);
};
</script>
Using the Directive
<template>
<div class="phone-wrapper">
<select v-model="selectedCountry">
<option value="US">🇺🇸 +1</option>
<option value="GB">🇬🇧 +44</option>
<option value="DE">🇩🇪 +49</option>
</select>
<input
v-phone-mask="{
country: selectedCountry,
onChange: handleChange
}"
class="phone-input"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { PMaskPhoneNumber } from '#imports';
const selectedCountry = ref('US');
const phone = ref<PMaskPhoneNumber>({
full: '',
fullFormatted: '',
digits: ''
});
const handleChange = (nextPhone: PMaskPhoneNumber) => {
phone.value = nextPhone;
};
</script>
SSR + Auto-imports in Practice
@desource/phone-mask-nuxt renders PhoneInput on the server and hydrates it in the browser. Browser locale, system theme, country detection, and the dropdown initialize after mount. Pass locale, theme, and country explicitly to control the initial server-rendered appearance.
<script setup lang="ts">
const phone = ref('');
const valid = ref(false);
</script>
<template>
<!-- Auto-imported: no explicit component import required -->
<PhoneInput id="phone" name="phone" v-model="phone" country="US" @validation-change="valid = $event" />
<p v-if="valid">✓ Ready to submit</p>
</template>
Backend Payload (Raw Digits)
<script setup lang="ts">
import type { PMaskPhoneNumber } from '#imports';
const phoneDigits = ref('');
const handlePhoneChange = async (phone: PMaskPhoneNumber) => {
await $fetch('/api/profile/phone', {
method: 'POST',
body: {
phoneDigits: phone.digits, // unformatted value for backend
phoneFull: phone.full // optional full number with country code
}
});
};
</script>
<template>
<PhoneInput v-model="phoneDigits" country="US" @change="handlePhoneChange" />
</template>
Auto-importing Helpers + Composable
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@desource/phone-mask-nuxt'],
phoneMask: {
// Disable auto-imported styles, component, and directive if you don't need them
css: false,
component: false,
directive: false,
// And enable only needed auto-imports
helpers: true,
composable: true
}
});
Then use them directly in SFCs without manual imports:
<script setup lang="ts">
const value = ref('');
const selectedCountry = ref('US');
const { inputRef, setCountry } = usePhoneMask({
value,
onChange: (digits) => (value.value = digits),
country: selectedCountry
});
const setGermany = () => {
setCountry('DE');
};
</script>
<template>
<input ref="inputRef" type="tel" />
<button @click="setGermany">Use Germany</button>
<p>Code for DE: {{ PMaskHelpers.MasksMap.DE.code }}</p>
</template>
⚙️ Configuration
Module Options
Configure the module in nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@desource/phone-mask-nuxt'],
phoneMask: {
// Import styles automatically
css: true, // Default: true
// Register PhoneInput component
component: true, // Default: true
// Register v-phone-mask directive
directive: true, // Default: true
// Register mask-related TypeScript types
types: true, // Default: true
// Register shared helper namespace + directive helper
helpers: false, // Default: false
// Register usePhoneMask composable
composable: false // Default: false
}
});
Enable Helpers and Composable Auto-imports
export default defineNuxtConfig({
modules: ['@desource/phone-mask-nuxt'],
phoneMask: {
helpers: true,
composable: true
}
});
Custom Styling
Option 1: Disable auto CSS import
export default defineNuxtConfig({
modules: ['@desource/phone-mask-nuxt'],
phoneMask: {
css: false // Don't auto-import styles
}
});
Then import manually where needed:
<style>
@import '@desource/phone-mask-vue/assets/lib.css';
/* Your custom overrides */
.desource-phone-input {
--pi-border: #your-color;
}
</style>
Option 2: Override CSS variables
<style>
.desource-phone-input,
.desource-phone-dropdown {
--pi-bg: #f9fafb;
--pi-border: #e5e7eb;
--pi-border-focus: #3b82f6;
--pi-fg: #111827;
}
</style>
🔧 TypeScript
The module provides automatic TypeScript support. You can import Nuxt-registered types from #imports:
import type {
PCountryKey,
PMaskBase,
PMaskBaseMap,
PMask,
PMaskMap,
PMaskWithFlag,
PMaskWithFlagMap,
PMaskFull,
PMaskFullMap,
PMaskPhoneNumber
} from '#imports';
📚 Examples
Form Integration
<template>
<form @submit.prevent="handleSubmit">
<div>
<label for="name">Name</label>
<input id="name" v-model="form.name" type="text" />
</div>
<div>
<label for="phone">Phone</label>
<PhoneInput id="phone" v-model="form.phone" country="US" @validation-change="phoneValid = $event" />
</div>
<button type="submit" :disabled="!phoneValid">Submit</button>
</form>
</template>
<script setup>
const form = reactive({
name: '',
phone: ''
});
const phoneValid = ref(false);
const handleSubmit = () => {
console.log('Form data:', form);
};
</script>
With Pinia Store
// stores/user.ts
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
phoneDigits: '',
country: 'US'
}),
actions: {
setPhoneDigits(phone: string) {
this.phoneDigits = phone;
},
setCountry(id: string) {
this.country = id;
}
}
});
<template>
<PhoneInput
:model-value="userStore.phoneDigits"
:country="userStore.country"
@update:model-value="userStore.setPhoneDigits"
@country-change="userStore.setCountry($event.id)"
/>
</template>
<script setup>
const userStore = useUserStore();
</script>
Multi-step Form
<template>
<div>
<div v-if="step === 1">
<h2>Step 1: Contact Info</h2>
<PhoneInput v-model="formData.phone" country="US" @validation-change="phoneValid = $event" />
<button @click="nextStep" :disabled="!phoneValid">Next</button>
</div>
<div v-if="step === 2">
<h2>Step 2: Verification</h2>
<p>We'll send a code to: {{ formData.phone }}</p>
<button @click="prevStep">Back</button>
<button @click="submit">Send Code</button>
</div>
</div>
</template>
<script setup>
const step = ref(1);
const phoneValid = ref(false);
const formData = reactive({
phone: ''
});
const nextStep = () => {
if (phoneValid.value) step.value++;
};
const prevStep = () => {
step.value--;
};
const submit = async () => {
// Send verification code
};
</script>
i18n Integration
<template>
<PhoneInput v-model="phone" :locale="$i18n.locale" :search-placeholder="$t('phone.searchPlaceholder')" />
</template>
<script setup>
const { locale, t } = useI18n();
const phone = ref('');
</script>
🎯 Auto-imports
By default (without extra options), Nuxt auto-imports:
Components
PhoneInput— Main phone input component
Directives
vPhoneMask— Phone mask directive
Types
PCountryKeyPMaskBase,PMaskBaseMapPMask,PMaskMapPMaskWithFlag,PMaskWithFlagMapPMaskFull,PMaskFullMapPMaskPhoneNumber
Enable phoneMask.helpers: true to auto-import:
vPhoneMaskSetCountry— Programmatically set country for directivePMaskHelpers— Core kit utilities and mask data from@desource/phone-mask-vue/core
Enable phoneMask.composable: true to auto-import:
usePhoneMask
🔄 Migration from Vue Plugin
If you're migrating from the Vue plugin:
Before:
// main.ts
import PhoneMaskPlugin from '@desource/phone-mask-vue';
import '@desource/phone-mask-vue/assets/lib.css';
app.use(PhoneMaskPlugin);
After:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@desource/phone-mask-nuxt']
});
No changes needed in your components!
📦 What's Included
PhoneInputcomponent (auto-imported by default)vPhoneMaskdirective (auto-imported by default)- Mask-related TypeScript types (auto-imported by default)
- Default styles (auto-imported by default, can be disabled)
vPhoneMaskSetCountryandPMaskHelpers(optional viaphoneMask.helpers)usePhoneMask(optional viaphoneMask.composable)
🔗 Related Packages
- @desource/phone-mask — Core library
- @desource/phone-mask-vue — Vue 3 component
- @desource/phone-mask-angular — Angular bindings
- @desource/phone-mask-react — React bindings
- @desource/phone-mask-svelte — Svelte bindings
📄 License
MIT © 2026 DeSource Labs
🤝 Contributing
See Contributing Guide for development, testing, and publication checks.