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.
<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.
<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.
<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.
<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.
<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.
<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.
<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.
<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
International format: +1 415 555 2671
<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><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.
<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>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
@focusand@blurevents - Type a number to test the
@update:valueevent and watch@isValidchange - Enter a valid US national number (e.g.
4155552671) to test the@nationalFormatevent
Event Log:
<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.
<div class="p-[3px]">
<!-- Your component here -->
</div>Accessibility
- Label the phone field with
aria-labelledby/aria-label, and reference format instructions viaaria-describedby. - When the country picker opens, keep the trigger’s
aria-expanded/aria-controlsvalues synchronized with the dial-code list id. - Mark validation issues with
aria-invalid="true"on the text input.
Imports
import { HLInputPhone, HLForm, HLFormItem } from '@platform-ui/highrise'Props
| Name | Type | Default | Description |
|---|---|---|---|
| id * | string | undefined | undefined | Unique identifier for the input |
| countryCode | string | undefined | undefined | Two-letter country code (ISO 3166-1 alpha-2). Defaults to US when omitted. |
| value | string | undefined | undefined | Phone number value. Bind with v-model:value (see the note below). |
| placeholder | string | undefined | undefined | Placeholder text. Overrides the example number shown for the selected country. |
| disabled | boolean | false | Whether the input is disabled |
| disableCountryPicker | boolean | false | Hides the country selection dropdown and forces national formatting |
| clearable | boolean | true | Whether a clear (✕) button is shown when the input has a value |
| autocomplete | boolean | false | When true, sets autocomplete="tel" so the browser can suggest saved numbers |
| size | 'lg' | 'md' | 'sm' | 'xs' | '2xs' | '3xs' | inherits form | Size 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). |
| dropdownHeight | string | '32rem' | Max height of the country picker dropdown (any CSS length) |
| fullWidthCountryPicker | boolean | false | Makes the country trigger and dropdown span the full input width and show full country names instead of just the flag + dial code |
| showSavedIcon | boolean | false | Shows a checkmark icon to indicate a saved state |
| inline | boolean | false | Renders as read-only text that becomes editable on click (inline edit mode) |
| showInlineCTA | boolean | false | In inline mode, shows confirm/cancel buttons instead of confirming on Enter |
| showInlineBottomBorder | boolean | true | In inline mode, whether the bottom border is shown while editing |
| fontSize | string | var(--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. |
| fontWeight | string | var(--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
| Name | Parameters | Description |
|---|---|---|
@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
| Name | Description |
|---|---|
| suffix | Content to show after the input (always visible) |
| edit-actions | Custom action buttons shown while editing in inline mode (replaces the default CTAs) |
Methods
| Name | Parameters | Returns | Description |
|---|---|---|---|
focus | () => void | void | Focus the phone input |
blur | () => void | void | Blur the phone input |
clear | () => void | void | Clear the input value |
select | () => void | void | Select the text in the input |
scrollTo | () => void | void | Scroll the input into view |