Skip to content
RTL Support: Full
Accessibility: Full
Translations: Not Needed

Input Phone

A component for entering and validating international phone numbers with country-code selection.

Basic Usage

Bind the phone number and country code with v-model:value and v-model:countryCode.

vue
<template>
  <HLInputPhone v-model:countryCode="countryCode" v-model:value="phone" />
</template>

<script setup lang="ts">
  import { HLInputPhone } from '@platform-ui/highrise'
  import { ref } from 'vue'
  const phone = ref('+91845403166')
  const countryCode = ref('IN')
</script>

INFO

The phone number is bound with v-model:value (named), not a bare v-model. A plain v-model binds modelValue, which this component does not expose, so two-way binding will silently not work. Always use v-model:value for the number and v-model:countryCode for the country. If you only need to set an initial country without tracking changes, pass a static country-code="US" instead.

With Dropdown Height

Use the dropdownHeight prop to set the height of the country picker dropdown.

vue
<template>
  <HLInputPhone v-model:value="phone" v-model:countryCode="countryCode" dropdownHeight="40vh" />
</template>
<script setup lang="ts">
  import { HLInputPhone } from '@platform-ui/highrise'
  import { ref } from 'vue'
  const phone = ref('+91845403166')
  const countryCode = ref('IN')
</script>

With Icon

Use the suffix slot to render an icon after the input.

vue
<template>
  <HLInputPhone v-model:countryCode="countryCode" v-model:value="phone">
    <template #suffix>
      <HLIcon>
        <MessageQuestionCircleIcon class="w-4" />
      </HLIcon>
    </template>
  </HLInputPhone>
</template>
<script setup lang="ts">
  import { HLInputPhone, HLIcon } from '@platform-ui/highrise'
  import { MessageQuestionCircleIcon } from '@gohighlevel/ghl-icons/24/outline'
  import { ref } from 'vue'
  const phone = ref('+91845403166')
  const countryCode = ref('IN')
</script>

Sizes

Use the size prop to set the input height. Supported values are lg, md, sm, xs, 2xs, and 3xs.

vue
<template>
  <HLInputPhone size="lg" placeholder="Large size" />
  <HLInputPhone size="md" placeholder="Medium size" />
  <HLInputPhone size="sm" placeholder="Small size" />
  <HLInputPhone size="xs" placeholder="Extra small size" />
  <HLInputPhone size="2xs" placeholder="2x Extra small size" />
  <HLInputPhone size="3xs" placeholder="3x Extra small size" />
</template>
<script setup lang="ts">
  import { HLInputPhone } from '@platform-ui/highrise'
  import { ref } from 'vue'
  const phone = ref('+91845403166')
  const countryCode = ref('IN')
</script>

Font Style

Use fontSize and fontWeight to override the text styling of the input. Both accept any CSS length / weight value, so a design token like var(--hr-font-size-2xl) or a raw 18px both work. They are applied after the size preset, so they win over whatever size would otherwise set — useful when you need a large display number in a compact field.

vue
<template>
  <!-- Default typography -->
  <HLInputPhone v-model:countryCode="countryCode" v-model:value="phone" />

  <!-- Larger, heavier text -->
  <HLInputPhone
    v-model:countryCode="countryCode"
    v-model:value="phone"
    fontSize="var(--hr-font-size-2xl)"
    fontWeight="var(--hr-font-weight-semibold)"
  />
</template>

<script setup lang="ts">
  import { HLInputPhone } from '@platform-ui/highrise'
  import { ref } from 'vue'
  const phone = ref('8454031669')
  const countryCode = ref('IN')
</script>

Disabled State

Set the disabled prop to prevent interaction with the input.

(201) 555-0123
vue
<template>
  <HLInputPhone id="input-phone-disabled" disabled />
</template>
<script setup lang="ts">
  import { HLInputPhone } from '@platform-ui/highrise'
</script>

With Disabled Country Picker

Set the disableCountryPicker prop to hide the country selection dropdown.

vue
<template>
  <HLInputPhone id="input-phone-no-country" disableCountryPicker v-model:value="phone" />
</template>
<script setup lang="ts">
  import { HLInputPhone } from '@platform-ui/highrise'
  import { ref } from 'vue'
  const phone = ref('+91845403166')
</script>

Full-Width Country Picker

Set fullWidthCountryPicker to make the country trigger and its dropdown span the full width of the input and show full country names alongside the flag and dial code, instead of the compact flag-only trigger.

vue
<template>
  <HLInputPhone
    v-model:countryCode="countryCode"
    v-model:value="phone"
    fullWidthCountryPicker
  />
</template>
<script setup lang="ts">
  import { HLInputPhone } from '@platform-ui/highrise'
  import { ref } from 'vue'
  const phone = ref('8454031669')
  const countryCode = ref('IN')
</script>

Format Types

The format prop controls the shape of the value emitted by @update:value (and therefore what your v-model:value holds) — national gives (415) 555-2671, international gives +1 415 555 2671. Regardless of format, the component always emits both @nationalFormat and @internationalFormat on every change, so you can capture either form independently of what you bind. When the picker is disabled (disableCountryPicker), formatting is forced to national.

INFO

Typing a number with an international dial code (e.g. +44 20 7946 0958) auto-detects the country: the component re-parses it and emits @update:countryCode, updating the flag to match. Both the value and country stay in sync with what the user types.

National format: (415) 555-2671

Try typing: 4155552671

International format: +1 415 555 2671

Try typing: 4155552671
vue
<template>
  <HLInputPhone
    v-model:countryCode="nationalCountryCode"
    v-model:value="nationalPhone"
    format="national"
    placeholder="Try typing: 4155552671"
    @nationalFormat="handleNationalFormat"
  />
</template>
<script setup lang="ts">
  import { HLInputPhone } from '@platform-ui/highrise'
  import { ref } from 'vue'
  const nationalPhone = ref('')
  const nationalCountryCode = ref('US')
  const handleNationalFormat = (formattedPhone: string) => {
    console.log('national format:', formattedPhone)
  }
</script>
vue
<template>
  <HLInputPhone
    v-model:countryCode="internationalCountryCode"
    v-model:value="internationalPhone"
    format="international"
    placeholder="Try typing: 4155552671"
    @internationalFormat="handleInternationalFormat"
  />
</template>
<script setup lang="ts">
  import { HLInputPhone } from '@platform-ui/highrise'
  import { ref } from 'vue'
  const internationalPhone = ref('')
  const internationalCountryCode = ref('US')
  const handleInternationalFormat = (formattedPhone: string) => {
    console.log('international format:', formattedPhone)
  }
</script>

With Form Validation

Drive the field's validity from the @isValid event rather than parsing the number yourself — the component already validates against the selected country with libphonenumber-js. Store the emitted boolean and reference it wherever you need it: from an HLFormItem rule's validator. @isValid fires on mount with the initial value and again on every change, so the state is correct from the first render.

vue
<template>
  <HLForm ref="formRef" :rules="rules">
    <HLFormItem path="phone">
      <HLInputPhone
        v-model:countryCode="countryCode"
        v-model:value="phone"
        @isValid="isFormPhoneValid = $event"
      />
    </HLFormItem>
  </HLForm>
</template>
<script setup lang="ts">
  import { HLForm, HLFormItem, HLInputPhone } from '@platform-ui/highrise'
  import { phone, countryCode, isFormPhoneValid, formRef, rules } from './options'
</script>
ts
import { ref } from 'vue'

export const formRef = ref(null)
export const phone = ref('+91845403166')
export const countryCode = ref('IN')
export const isFormPhoneValid = ref(true)

export const rules = {
  phone: {
    required: true,
    validator(_: unknown, updatedPhone: string) {
      if (isFormPhoneValid.value) return true
      if (!updatedPhone) {
        return new Error('Phone number is required')
      }
      if (/[a-zA-Z]/g.test(updatedPhone)) {
        return new Error('Phone number can only contain numbers')
      }
      if (!isFormPhoneValid.value) {
        return new Error('Please enter a valid phone number')
      }
    },
    trigger: ['input', 'blur', 'change', 'focus'],
  },
}

Event Testing

This example logs the events the component emits as you interact with it. Try the following:

  • Focus and blur the input to test the @focus and @blur events
  • Type a number to test the @update:value event and watch @isValid change
  • Enter a valid US national number (e.g. 4155552671) to test the @nationalFormat event

Event Log:

No events logged yet. Try the actions above.
vue
<template>
  <HLInputPhone
    v-model:countryCode="countryCode"
    v-model:value="phone"
    placeholder="Test events here..."
    @focus="addEventLog('Focus event triggered')"
    @blur="addEventLog('Blur event triggered')"
    @update:value="val => addEventLog('Value updated: ' + val)"
    @isValid="valid => addEventLog('Validity changed: ' + valid)"
    @nationalFormat="val => addEventLog('National format: ' + val)"
  />
  <div class="text-sm">
    <p class="font-bold mb-2">Event Log:</p>
    <div v-if="eventLog.length === 0" class="text-gray-500">No events logged yet. Try the actions above.</div>
    <div v-for="(log, index) in eventLog" :key="index" class="text-gray-700">{{ log.timestamp }}: {{ log.event }}</div>
  </div>
</template>

<script setup lang="ts">
  import { HLInputPhone } from '@platform-ui/highrise'
  import { ref } from 'vue'

  const phone = ref('+91845403166')
  const countryCode = ref('IN')
  const eventLog = ref<{ event: string; timestamp: string }[]>([])

  const addEventLog = (event: string) => {
    eventLog.value.unshift({ event, timestamp: new Date().toLocaleTimeString() })
    // Keep only last 5 events
    if (eventLog.value.length > 5) {
      eventLog.value.pop()
    }
  }
</script>

Design Guidelines

Input components use a box-shadow to render their focus ring. Box-shadows render outside the element's bounds and may be clipped by any ancestor using overflow: hidden (e.g. Tab Panels or Dropdown Menus).

To prevent this, add a small gutter padding to the component's wrapper to ensure there is enough room for the focus ring to render without being cut off.

vue
<div class="p-[3px]">
  <!-- Your component here -->
</div>

Accessibility

  • Label the phone field with aria-labelledby / aria-label, and reference format instructions via aria-describedby.
  • When the country picker opens, keep the trigger’s aria-expanded / aria-controls values synchronized with the dial-code list id.
  • Mark validation issues with aria-invalid="true" on the text input.

Imports

ts
import { HLInputPhone, HLForm, HLFormItem } from '@platform-ui/highrise'

Props

NameTypeDefaultDescription
id *string | undefinedundefinedUnique identifier for the input
countryCodestring | undefinedundefinedTwo-letter country code (ISO 3166-1 alpha-2). Defaults to US when omitted.
valuestring | undefinedundefinedPhone number value. Bind with v-model:value (see the note below).
placeholderstring | undefinedundefinedPlaceholder text. Overrides the example number shown for the selected country.
disabledbooleanfalseWhether the input is disabled
disableCountryPickerbooleanfalseHides the country selection dropdown and forces national formatting
clearablebooleantrueWhether a clear (✕) button is shown when the input has a value
autocompletebooleanfalseWhen true, sets autocomplete="tel" so the browser can suggest saved numbers
size'lg' | 'md' | 'sm' | 'xs' | '2xs' | '3xs'inherits formSize of the input. Falls back to the surrounding HLForm / HLInputGroup size, then 'sm'.
format'national' | 'international''national'Format used for the value emitted by @update:value. Ignored when disableCountryPicker is set (always national).
dropdownHeightstring'32rem'Max height of the country picker dropdown (any CSS length)
fullWidthCountryPickerbooleanfalseMakes the country trigger and dropdown span the full input width and show full country names instead of just the flag + dial code
showSavedIconbooleanfalseShows a checkmark icon to indicate a saved state
inlinebooleanfalseRenders as read-only text that becomes editable on click (inline edit mode)
showInlineCTAbooleanfalseIn inline mode, shows confirm/cancel buttons instead of confirming on Enter
showInlineBottomBorderbooleantrueIn inline mode, whether the bottom border is shown while editing
fontSizestringvar(--hr-font-size-lg)Font size of the input text. Any CSS length. Applied after the size preset, so it overrides the size's font styling. Works in both normal and inline mode.
fontWeightstringvar(--hr-font-weight-normal)Font weight of the input text. Applied after the size preset, so it overrides the size's font styling. Works in both normal and inline mode.

Emits

NameParametersDescription
@internationalFormat(phone: string)Emitted on every change with the international form (e.g. +1 415 555 2671); empty string when the number can't be parsed
@nationalFormat(phone: string)Emitted on every change with the national form (e.g. (415) 555-2671); empty string when the number can't be parsed
@isValid(valid: boolean)Emitted on mount and on every change with the current validity
@update:countryCode(code: string)Emitted when the country changes — including auto-detection from a typed dial code
@update:value(value: string)Emitted when the value changes, formatted per the format prop
@focus(event: FocusEvent)Emitted when the input receives focus
@blur(event: FocusEvent)Emitted when the input loses focus
@keydown(event: KeyboardEvent)Emitted on keydown in the input
@confirm(value: string)Inline mode only: emitted when an edit is confirmed (Enter or CTA)
@cancel(value: string)Inline mode only: emitted when an edit is cancelled (Esc or CTA)

Slots

NameDescription
suffixContent to show after the input (always visible)
edit-actionsCustom action buttons shown while editing in inline mode (replaces the default CTAs)

Methods

NameParametersReturnsDescription
focus() => voidvoidFocus the phone input
blur() => voidvoidBlur the phone input
clear() => voidvoidClear the input value
select() => voidvoidSelect the text in the input
scrollTo() => voidvoidScroll the input into view