<script setup lang="ts">
import { ref } from 'vue'
import {
PhoneInput,
PhoneInputClear,
PhoneInputCountrySelect,
PhoneInputField,
type CountryCode,
} from '@/components/ui/phone-input'
const phone = ref('')
const country = ref<CountryCode>('US')
</script>
<template>
<div class="w-full max-w-sm">
<PhoneInput v-model="phone" v-model:country="country">
<PhoneInputCountrySelect />
<PhoneInputField placeholder="Phone number" />
<PhoneInputClear />
</PhoneInput>
</div>
</template>The PhoneInput component provides a robust solution for capturing and validating international phone numbers, complete with an auto-detecting country dropdown, dynamic formatting, and localized country names.
Installation
pnpm dlx sulaf@latest add phone-input
Usage
<script setup lang="ts">
import { ref } from 'vue'
import {
PhoneInput,
PhoneInputClear,
PhoneInputCountrySelect,
PhoneInputField,
type CountryCode
} from '@/components/ui/phone-input'
const phone = ref('')
const country = ref<CountryCode>('US')
</script>
<template>
<PhoneInput
v-model="phone"
v-model:country="country"
>
<PhoneInputCountrySelect />
<PhoneInputField placeholder="Phone number" />
<PhoneInputClear />
</PhoneInput>
</template>Examples
Variants
The component supports visual variants to indicate success, warning, or danger states out of the box.
<script setup lang="ts">
import { ref } from 'vue'
import {
PhoneInput,
PhoneInputClear,
PhoneInputCountrySelect,
PhoneInputField,
type CountryCode,
} from '@/components/ui/phone-input'
import { Label } from '~/components/ui/label'
const phone1 = ref('+1 415 555 2671')
const country1 = ref<CountryCode>('US')
const phone2 = ref('+1 415 555 2671')
const country2 = ref<CountryCode>('US')
const phone3 = ref('+1 415 555 2671')
const country3 = ref<CountryCode>('US')
const phone4 = ref('+1 415 555 2671')
const country4 = ref<CountryCode>('US')
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-6">
<div class="flex flex-col gap-2">
<Label>Default</Label>
<PhoneInput v-model="phone1" v-model:country="country1" variant="default">
<PhoneInputCountrySelect />
<PhoneInputField placeholder="Phone number" />
<PhoneInputClear />
</PhoneInput>
</div>
<div class="flex flex-col gap-2">
<Label>Success</Label>
<PhoneInput v-model="phone2" v-model:country="country2" variant="success">
<PhoneInputCountrySelect />
<PhoneInputField placeholder="Phone number" />
<PhoneInputClear />
</PhoneInput>
</div>
<div class="flex flex-col gap-2">
<Label>Warning</Label>
<PhoneInput v-model="phone3" v-model:country="country3" variant="warning">
<PhoneInputCountrySelect />
<PhoneInputField placeholder="Phone number" />
<PhoneInputClear />
</PhoneInput>
</div>
<div class="flex flex-col gap-2">
<Label>Danger</Label>
<PhoneInput v-model="phone4" v-model:country="country4" variant="danger">
<PhoneInputCountrySelect />
<PhoneInputField placeholder="Phone number" />
<PhoneInputClear />
</PhoneInput>
</div>
</div>
</template>Validation State
You can use the exposed validation utilities to dynamically calculate and style the input based on whether the provided number is valid for the selected country.
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
PhoneInput,
PhoneInputClear,
PhoneInputCountrySelect,
PhoneInputField,
validatePhoneNumber,
type CountryCode,
} from '@/components/ui/phone-input'
import { Label } from '~/components/ui/label'
const phone = ref('')
const country = ref<CountryCode>('US')
const validation = computed(() => {
if (!phone.value) return 'empty'
const result = validatePhoneNumber(phone.value, country.value)
return result.success ? 'valid' : result.error
})
const variant = computed(() => {
if (validation.value === 'empty') return 'default'
if (validation.value === 'valid') return 'success'
return 'danger'
})
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="flex flex-col gap-2">
<Label>Validation State</Label>
<PhoneInput v-model="phone" v-model:country="country" :variant="variant">
<PhoneInputCountrySelect />
<PhoneInputField placeholder="Enter a valid phone number" />
<PhoneInputClear />
</PhoneInput>
</div>
<div class="text-sm text-muted-foreground">
Current state:
<span class="font-medium text-foreground">{{ validation }}</span>
</div>
</div>
</template>Phone Format
Control the formatting of the phone number as it's entered. Supports international (default), national, and e164.
<script setup lang="ts">
import { ref } from 'vue'
import {
PhoneInput,
PhoneInputClear,
PhoneInputCountrySelect,
PhoneInputField,
type CountryCode,
type PhoneFormat,
} from '@/components/ui/phone-input'
import { Label } from '~/components/ui/label'
import { Button } from '~/components/ui/button'
const phone = ref('+1 415 555 2671')
const country = ref<CountryCode>('US')
const format = ref<PhoneFormat>('international')
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="flex flex-col gap-2">
<Label
>Format: <span class="capitalize">{{ format }}</span></Label
>
<PhoneInput v-model="phone" v-model:country="country" :format="format">
<PhoneInputCountrySelect />
<PhoneInputField placeholder="Phone number" />
<PhoneInputClear />
</PhoneInput>
</div>
<div class="flex gap-2">
<Button
size="sm"
:variant="format === 'international' ? 'default' : 'outline'"
@click="format = 'international'"
>
International
</Button>
<Button
size="sm"
:variant="format === 'national' ? 'default' : 'outline'"
@click="format = 'national'"
>
National
</Button>
<Button
size="sm"
:variant="format === 'e164' ? 'default' : 'outline'"
@click="format = 'e164'"
>
E164
</Button>
</div>
</div>
</template>Disabled
Display the phone input in a disabled state.
<script setup lang="ts">
import { ref } from 'vue'
import {
PhoneInput,
PhoneInputClear,
PhoneInputCountrySelect,
PhoneInputField,
type CountryCode,
} from '@/components/ui/phone-input'
import { Label } from '~/components/ui/label'
const phone = ref('+1 415 555 2671')
const country = ref<CountryCode>('US')
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-2">
<Label>Disabled</Label>
<PhoneInput v-model="phone" v-model:country="country" disabled>
<PhoneInputCountrySelect />
<PhoneInputField placeholder="Phone number" />
<PhoneInputClear />
</PhoneInput>
</div>
</template>API Reference
PhoneInput (Root)
| Prop | Type | Default | Description |
|---|---|---|---|
v-model | string | '' | The formatted phone number value. |
v-model:country | CountryCode | 'YE' (or defaultCountry) | The currently selected country code. |
defaultCountry | CountryCode | 'YE' | The default country code when no country is selected. |
countries | CountryOption[] | (Generated) | A customized list of countries to display in the dropdown. |
disabled | boolean | false | Disables the phone input and dropdown. |
variant | 'default' | 'success' | 'warning' | 'danger' | 'default' | Visual variant for the input. |
placeholder | string | 'Phone number' | Placeholder text for the input field. |
format | 'international' | 'national' | 'e164' | 'national' | Preferred format for the generated phone number string. |
locale | string | (Browser language) | The locale used for translating country names. |
name | string | 'phone' | Name attribute for the hidden input (for forms). |
required | boolean | false | Marks the input as required. |
autocomplete | string | 'tel' | Autocomplete attribute for the input. |
autoDetectCountry | boolean | true | Automatically switch the country when the typed number reveals a different country code. |
Emits
@update:modelValue(payload: string | undefined)@update:country(payload: CountryCode)@onCountryChange(payload: CountryCode)@onClear()
PhoneInputCountrySelect
| Prop | Type | Default | Description |
|---|---|---|---|
showCountryName | boolean | false | Display the country name in the dropdown items alongside the flag and calling code. |
Utility Functions
The package exposes several utility functions from the validation.ts and utils.ts modules for working with phone numbers and countries programmatically.
| Function | Signature | Description |
|---|---|---|
validatePhoneNumber | (phone: string, country?: CountryCode) => PhoneValidationResult | Validates a phone number and returns a detailed result (success, or specific error like TOO_SHORT, INVALID_COUNTRY). |
getPhoneValidationState | (phone: string, country?: CountryCode) => PhoneValidationState | Returns a simplified string state: 'empty', 'valid', or a validation error. |
isValidPhoneNumber | (phone: string) => boolean | Quick boolean check if a phone number is valid globally (usually requires international format with +). |
isValidPhoneNumberForCountry | (phone: string, country: string) => boolean | Quick boolean check if a phone number is valid for a specific country. |
parsePhone | (value: string, countryCode: CountryCode) => PhoneNumber | null | Safely parses a string into a PhoneNumber object from libphonenumber-js. |
buildCountryOptions | (locale?: string) => CountryOption[] | Generates a localized array of countries including their name, flag, and calling code. |