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

Input OTP

Component for OTP Input.

Default

Renders an OTP input with the default six fields.

vue
<template>
  <HLInputOtp @onComplete="onCompleteHandler" @onChange="onChangeHandler" />
</template>

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

  const otpValue = ref('')
  const onChangeHandler = (value: string) => {
    // do something
  }
  const onCompleteHandler = (value: { otp: string; state: 'completed' }) => {
    otpValue.value = value.otp
  }
</script>

Fields

Sets the number of input fields with the fields prop.

vue
<template>
  <HLInputOtp size="sm" :fields="4" @onComplete="onCompleteHandler" @onChange="onChangeHandler" />
</template>

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

  const otpValue = ref('')
  const onChangeHandler = (value: string) => {
    // do something
  }
  const onCompleteHandler = (value: { otp: string; state: 'completed' }) => {
    otpValue.value = value.otp
  }
</script>

Sizes

The size prop scales the fields. It accepts lg, md (default), sm, xs, 2xs, and 3xs.

vue
<template>
  <HLInputOtp size="lg" :fields="4" />
  <HLInputOtp size="md" :fields="4" />
  <HLInputOtp size="sm" :fields="4" />
  <HLInputOtp size="xs" :fields="4" />
  <HLInputOtp size="2xs" :fields="4" />
  <HLInputOtp size="3xs" :fields="4" />
</template>

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

Separator Position

Inserts a separator after the field index given by separatorPosition.

vue
<template>
  <HLInputOtp size="sm" :fields="6" :separatorPosition="3" @onComplete="onCompleteHandler" @onChange="onChangeHandler" />
</template>

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

  const otpValue = ref('')
  const onChangeHandler = (value: string) => {
    // do something
  }
  const onCompleteHandler = (value: { otp: string; state: 'completed' }) => {
    otpValue.value = value.otp
  }
</script>

Custom Placeholder

Replaces the default placeholder character with the placeholder prop.

vue
<template>
  <HLInputOtp size="sm" :fields="4" placeholder="#" @onComplete="onCompleteHandler" @onChange="onChangeHandler" />
</template>

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

  const otpValue = ref('')
  const onChangeHandler = (value: string) => {
    // do something
  }
  const onCompleteHandler = (value: { otp: string; state: 'completed' }) => {
    otpValue.value = value.otp
  }
</script>

Verification Form

The most common use is verifying a code sent to the user. Listen for @onComplete to validate the full code, and toggle status to error when it doesn't match. Wrapping the field in an HLFormItem renders the error message. In this demo the expected code is 123456.

vue
<template>
  <HLFormItem label="Enter the 6-digit code" :validation-status="status" :feedback="feedback">
    <HLInputOtp :fields="6" :status="status" @onComplete="handleVerify" @onChange="handleChange" />
  </HLFormItem>
</template>

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

  const EXPECTED_CODE = '123456'
  const status = ref<'default' | 'error'>('default')
  const feedback = ref('')

  const handleVerify = (value: { otp: string; state: 'completed' }) => {
    if (value.otp === EXPECTED_CODE) {
      status.value = 'default'
      feedback.value = ''
      // proceed — code is valid
    } else {
      status.value = 'error'
      feedback.value = 'That code is incorrect. Try again.'
    }
  }

  // Clear the error as soon as the user edits the code
  const handleChange = () => {
    if (status.value === 'error') {
      status.value = 'default'
      feedback.value = ''
    }
  }
</script>

Disabled

Prevents input when the disabled prop is set.

vue
<template>
  <HLInputOtp size="sm" :fields="4" disabled @onComplete="onCompleteHandler" @onChange="onChangeHandler" />
</template>

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

  const otpValue = ref('')
  const onChangeHandler = (value: string) => {
    // do something
  }
  const onCompleteHandler = (value: { otp: string; state: 'completed' }) => {
    otpValue.value = value.otp
  }

</script>

Event Testing

This example logs the events the component emits as you type. @onChange fires on every keystroke with the current combined value; @onComplete fires once all fields are filled.

Event Log:

No events logged yet. Type a code above.
vue
<template>
  <HLInputOtp
    size="sm"
    :fields="4"
    @onChange="val => addEventLog('@onChange → ' + val)"
    @onComplete="val => addEventLog('@onComplete → ' + val.otp)"
  />
  <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. Type a code 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 { ref } from 'vue'
  import { HLInputOtp } from '@platform-ui/highrise'

  const eventLog = ref<{ event: string; timestamp: string }[]>([])
  const addEventLog = (event: string) => {
    eventLog.value.unshift({ event, timestamp: new Date().toLocaleTimeString() })
    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

  • Describe the OTP request via aria-label / aria-labelledby on the container (e.g., “Enter the 6-digit code”).
  • Give each cell an aria-label that announces its index (“Digit 2 of 6”) or hook helper text through aria-describedby.
  • Announce expired or incorrect codes inside an aria-live="assertive" region near the group.

Imports

ts
import { HLInputOtp } from '@platform-ui/highrise'

Props

NameTypeDefaultDescription
idstringAuto-generatedUnique identifier for the container. When omitted, an id is generated for accessibility.
fieldsnumber6Number of input fields to display
disabledbooleanfalseDisables the OTP input
placeholderstring'0'Placeholder character shown in each empty field
size'3xs' | '2xs' | 'xs' | 'sm' | 'md' | 'lg''md'Size of the OTP fields. Inherits from a surrounding form when unset.
status'default' | 'error''default'Visual state of the fields. The error styling renders when the OTP is inside an HLFormItem with validation-status="error" — see Verification Form.
separatorPositionnumber | undefinedundefinedInserts a separator after this many fields (e.g. 3 splits 6 fields into two groups of three).

Emits

NameParametersDescription
@onChange(otp: string)Fired on every keystroke with the current combined value.
@onComplete({ otp: string, state: 'completed' })Fired once all fields are filled, with the full code and completion state.