GitHubX

PhoneField

PreviousNext

A composable phone input component with country selection, auto-formatting, and validation

      
      <script setup lang="ts">
import { PhoneField, PhoneFieldCountrySelect, PhoneFieldInput } from "@/components/phone-field";
import { ref } from "vue";

const phone = ref<string>("");
const countryCode = ref<string>("FR");
</script>

<template>
    <PhoneField v-model="phone" v-model:country-code="countryCode" :preferred-countries="['FR', 'US']" reset-on-country-change>
        <PhoneFieldCountrySelect search-placeholder="Search country" flag-type="cdn" />
        <PhoneFieldInput placeholder="Enter your phone number" />
    </PhoneField>
</template>
    

Features

  • Auto-formatting — Phone numbers are formatted in real-time as the user types
  • Country selector — Searchable dropdown with flag display
  • Multiple formats — International, national, or E.164 output
  • Country filtering — Preferred, available, and ignored country lists
  • Localization — Country names displayed in any locale
  • Validation utilities — Ready-to-use validation functions
  • Composable — Flexible slot-based architecture

Installation

Install from the Vuzeno registry with the shadcn-vue CLI:

      
      bunx --bun shadcn-vue@latest add https://vuzeno.com/r/phone-field.json
    

Dependencies

This component is built on top of libphonenumber-js, for manual installation:

      
      bun add libphonenumber-js
    

Examples

With indicator

      
      <script setup lang="ts">
import { PhoneField, PhoneFieldCountrySelect, PhoneFieldIndicator, PhoneFieldInput } from "@/components/phone-field";
import { ref } from "vue";

const phone = ref<string>("");
const countryCode = ref<string>("FR");
</script>

<template>
    <PhoneField v-model="phone" v-model:country-code="countryCode" :preferred-countries="['FR', 'US']" reset-on-country-change>
        <PhoneFieldCountrySelect flag-type="cdn" />
        <PhoneFieldInput placeholder="Enter your phone number">
            <PhoneFieldIndicator />
        </PhoneFieldInput>
    </PhoneField>
</template>
    

Sizes

      
      <script setup lang="ts">
import { PhoneField, PhoneFieldCountrySelect, PhoneFieldInput } from "@/components/phone-field";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { ref } from "vue";

const phone = ref<string>("");
const countryCode = ref<string>("FR");
</script>

<template>
  <FieldGroup class="w-96">
    <Field>
      <FieldLabel>Small</FieldLabel>
      <PhoneField v-model="phone" v-model:country-code="countryCode" :preferred-countries="['FR', 'US']" reset-on-country-change size="sm">
        <PhoneFieldCountrySelect flag-type="cdn" />
        <PhoneFieldInput placeholder="Enter your phone number" />
      </PhoneField>
    </Field>

    <Field>
      <FieldLabel>Default</FieldLabel>
      <PhoneField v-model="phone" v-model:country-code="countryCode" :preferred-countries="['FR', 'US']" reset-on-country-change>
        <PhoneFieldCountrySelect flag-type="cdn" />
        <PhoneFieldInput placeholder="Enter your phone number" />
      </PhoneField>
    </Field>

    <Field>
      <FieldLabel>Large</FieldLabel>
      <PhoneField v-model="phone" v-model:country-code="countryCode" :preferred-countries="['FR', 'US']" reset-on-country-change size="lg">
        <PhoneFieldCountrySelect flag-type="cdn" />
        <PhoneFieldInput placeholder="Enter your phone number" />
      </PhoneField>
    </Field>
  </FieldGroup>
</template>
    

Formats

The format prop controls how the phone number is displayed:

      
      <script setup lang="ts">
import { PhoneField, PhoneFieldCountrySelect, PhoneFieldInput } from "@/components/phone-field";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { ref } from "vue";

const phone = ref<string>("");
const countryCode = ref<string>("FR");
</script>

<template>
  <FieldGroup class="w-96">
    <Field>
      <FieldLabel>National</FieldLabel>
      <PhoneField v-model="phone" v-model:country-code="countryCode" format="national" :preferred-countries="['FR', 'US']" reset-on-country-change>
        <PhoneFieldCountrySelect flag-type="cdn" />
        <PhoneFieldInput placeholder="Enter your phone number" />
      </PhoneField>
    </Field>

    <Field>
      <FieldLabel>International</FieldLabel>
      <PhoneField v-model="phone" v-model:country-code="countryCode" format="international" :preferred-countries="['FR', 'US']" reset-on-country-change>
        <PhoneFieldCountrySelect flag-type="cdn" />
        <PhoneFieldInput placeholder="Enter your phone number" />
      </PhoneField>
    </Field>
  </FieldGroup>
</template>
    

Note: The modelValue always stores the full E.164 number regardless of display format.

Country Filtering

Control which countries appear in the selector:

PropPurpose
preferredCountriesShown at the top of the list
availableCountriesRestrict to only these countries
ignoredCountriesHide specific countries

Countries in preferredCountries appear first, followed by remaining availableCountries. Any country in ignoredCountries is excluded.

Localization

Country names are formatted using Intl.DisplayNames. Set the locale prop to display names in any language:

  • "en" → United States, France, Germany
  • "fr" → États-Unis, France, Allemagne
  • "de" → Vereinigte Staaten, Frankreich, Deutschland

Validation

Validation Utilities

Three validation functions are exported for different use cases:

validatePhoneNumber(phone, country?)

Returns a detailed result object:

      
      type PhoneValidationResult =
  | { success: true }
  | { success: false; error: PhoneValidationError }

type PhoneValidationError = 
  | "TOO_SHORT" 
  | "TOO_LONG" 
  | "INVALID_COUNTRY" 
  | "INVALID_NUMBER" 
  | "INVALID_FORMAT"
    

isValidPhoneNumber(phone)

Simple boolean check. Returns true if the phone number is valid.

isValidPhoneNumberForCountry(phone, country)

Boolean check for a specific country. Useful when validating national format numbers.

Integration with TanStack Form

The validation utilities can be used with @tanstack/vue-form by calling them inside a field validator:

      
      <script>
import { validatePhoneNumber } from "@/components/ui/phone-field"
</script>

<template>
  <form.Field 
    name="phone"
    :validators="
      onChange: ({ value }) => {
        const result = validatePhoneNumber(value)
        if (!result.success) {
          return "Invalid phone number"
        }
      }
    "
    v-slot="{ field }"
  >
    <Field>
      <FieldLabel>Phone number</FieldLabel>
      <PhoneField
        :model-value="field.state.value" 
        default-country-code="FR"
        @update:model-value="field.handleChange"
      >
        <PhoneFieldCountrySelect flag-type="cdn" />
        <PhoneFieldInput placeholder="Enter your phone number">
          <PhoneFieldIndicator />
        </PhoneFieldInput>
      </PhoneField>

      <FieldError :errors="field.state.errors" />
    </Field>
  </form.Field>
</template>