Skip to content

OTP Fields

OTP fields are used to allow users to input a one-time password or a code. Usually used for 2FA (MFA) or authentication purposes.

Features

  • Use input or any other element to create an OTP field and its slots.
  • Labels, descriptions, and error message displays are automatically linked to input and label elements with aria-* attributes.
  • Validation support with native HTML constraint validation or Standard Schema validation.
  • Support for v-model binding.
  • Supports masking (hiding) the entered characters with a custom character.
  • Supports prefixes (e.g. F-{code}).
  • Supports custom length.
  • Supports both numeric and alphanumeric OTPs.
  • Comprehensively supports keyboard navigation.
  • Auto management of focus during user interaction.
  • Handles paste events.
  • Auto submit on completion.

Keyboard Features

KeyDescription
ArrowRight
Moves the focus to the next OTP slot.
ArrowLeft
Moves the focus to the previous OTP slot.
Backspace
Clears the current OTP slot and moves the focus to the previous slot.
Tab
Moves the focus to the next OTP slot.
Enter
Moves the focus to the next OTP slot.

Anatomy

Enter your code
Label
123-
OTP Slot
Control
Invalid code
Error Message or Description

Building an OTP Field Component

First, import the useOtpField composable and use it in your OTP field component.

The useOtpField composable returns binding objects for the elements shown in the anatomy. You will use v-bind to bind them to the corresponding DOM elements.

<script setup lang="ts">
import OtpField from './OtpField.vue';
</script>
<template>
<OtpField label="Your code" />
</template>
<template>
<div class="otp-field">
<div class="otp-field__label" v-bind="labelProps">{{ label }}</div>
<div class="otp-field__control" v-bind="controlProps">
<OtpSlot
v-for="slot in fieldSlots"
v-bind="slot"
class="otp-field__slot"
/>
</div>
<span v-bind="errorMessageProps" class="otp-field__error-message">
{{ errorMessage }}
</span>
</div>
</template>
<script setup lang="ts">
import { useOtpField, type OtpFieldProps, OtpSlot } from '@formwerk/core';
const props = defineProps<OtpFieldProps>();
const {
controlProps,
labelProps,
errorMessage,
errorMessageProps,
fieldSlots,
} = useOtpField(props);
</script>
58 collapsed lines
<style scoped>
.otp-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
--fw-border-color: #aaa;
--fw-input-bg: #fff;
--fw-text-color: #000;
--fw-error-color: #ff0000;
--fw-focus-color: #10b981;
--fw-disabled-bg: #e4e4e7;
}
.otp-field__label {
font-size: 14px;
font-weight: 400;
color: var(--fw-text-color);
}
.otp-field__control {
display: flex;
gap: 0.5rem;
}
.otp-field__slot {
height: 2.5rem;
width: 2.5rem;
border-radius: 0.375rem;
border: 1px solid var(--fw-border-color);
background-color: var(--fw-input-bg);
align-content: center;
text-align: center;
font-size: 1.3rem;
font-weight: 500;
color: var(--fw-text-color);
vertical-align: middle;
&:focus {
outline: 1px solid #059669;
background-color: #f0fdf4;
caret-color: transparent;
}
&:disabled {
background-color: var(--fw-disabled-bg);
opacity: 0.5;
}
}
.otp-field__error-message {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--fw-error-color);
font-size: 0.875rem;
}
</style>

For your convenience, Formwerk already implements an OTP Slot component that you can use in your OTP field directly, but you can still build your own with useOtpSlot.

Validation

HTML Constraints

You can use the following properties to validate the OTP field with native HTML constraint validation:

NameTypeDescription
requiredbooleanWhether the text field is required.
<script setup lang="ts">
import OtpField from './OtpField.vue';
</script>
<template>
<OtpField label="Your code" required />
</template>

Standard Schema

useOtpField also supports Standard Schema validation through the schema prop. This includes multiple providers like Zod, Valibot, Arktype, and more.

<script setup lang="ts">
import OtpField from './OtpField.vue';
import { z } from 'zod';
const schema = z.string().length(6);
</script>
<template>
<OtpField label="Your code" :schema="schema" />
</template>

Mixed Validation

All OTP fields created with Formwerk support mixed validation, which means you can use both HTML constraints and Standard Schema validation to validate the field, and they work seamlessly together.

Note that HTML constraints are validated first, so any errors from the HTML constraints will be displayed first. Then, once all HTML constraints are satisfied, the Standard Schema is validated.

<script setup lang="ts">
import OtpField from './OtpField.vue';
import { z } from 'zod';
const schema = z.string().length(6).startsWith('abc');
</script>
<template>
<OtpField label="Your code" :schema="schema" required />
</template>

This makes schemas lighter; however, we recommend sticking to one or the other per form for maintainability.

If you need to disable the native validation, you can do so by setting the disableHtmlValidation prop to true.

Usage

Disabled

Use disabled to mark fields as non-interactive. Disabled fields are not validated and are not submitted.

If you need to prevent the user from interacting with the field while still allowing it to submit, consider using readonly instead.

<script setup lang="ts">
import OtpField from './OtpField.vue';
</script>
<template>
<OtpField label="Disabled Code" disabled value="123456" />
</template>

Readonly

Use readonly to mark fields as non-editable. Readonly fields are still validated and submitted.

<script setup lang="ts">
import OtpField from './OtpField.vue';
</script>
<template>
<OtpField label="Readonly Code" readonly value="123456" />
</template>

Accepting Specific Characters

The OTP field accepts a accept prop to specify the type of characters that can be entered.

You can have one of the following values:

ValueDescription
alphanumericAlphanumeric characters are accepted (i.e: English letters and numbers), this is the default value.
numericOnly numeric characters are accepted.
allAll characters are accepted.
<script setup lang="ts">
import OtpField from './OtpField.vue';
</script>
<template>
<OtpField label="Numeric Code" accept="numeric" />
</template>

Masking Characters

The OTP field accepts a mask prop to specify the character to use for masking the entered characters, your model and submitted values be the actual values. The mask prop can either be a boolean which will apply a masked default, or a character that will be used to mask the entered value.

<script setup lang="ts">
import OtpField from './OtpField.vue';
</script>
<template>
<OtpField label="Secret Code" mask />
<OtpField label="Custom mask" mask="*" />
</template>

Prefix

OTP fields can have a prefix, prefixes cannot be changed or edited by the user.

<script setup lang="ts">
import OtpField from './OtpField.vue';
</script>
<template>
<OtpField label="Prefixed Code" prefix="F-" />
</template>

Custom Length

OTP fields accept a length prop to specify the number of OTP slots. By default the length is 6 without a prefix, and with a prefix it will be 4.

<script setup lang="ts">
import OtpField from './OtpField.vue';
</script>
<template>
<OtpField label="Custom Length" length="5" />
</template>

onCompleted Handler

The OTP field accepts an onCompleted handler to be notified when the user has filled all the OTP slots with valid characters.

<script setup lang="ts">
import OtpField from './OtpField.vue';
function onCompleted(value: string) {
alert(`Code completed: ${value}`);
}
</script>
<template>
<OtpField label="Enter Code" @completed="onCompleted" />
</template>

RTL

At this time, OTP fields do not support RTL (right-to-left) text direction. This is mainly because we want to get more feedback on this, from personal experience OTP codes are still LTR even in RTL web apps.

Feel free to open an issue on GitHub if you have any feedback on this.

API

useOtpField

Props

These are the properties that can be passed to the useOtpField composable.

NameTypeDescription
acceptThe type of the OTP field characters.
descriptionThe description of the OTP field.
disabledWhether the OTP field is disabled.
disableHtmlValidationWhether to disable HTML validation.
labelThe label of the OTP field.
lengthThe length of the OTP field characters.
maskWhether the OTP field is masked.
modelValueThe model value of the OTP field.
nameThe name of the OTP field.
onCompletedThe callback function that is called when the OTP field is completed.
prefixThe prefix of the OTP field. If you prefix your codes with a character, you can set it here (e.g "G-").
readonlyWhether the OTP field is readonly.
requiredWhether the OTP field is required.
schemaSchema for field validation.
valueThe initial value of the OTP field.

Returns

These are the properties in the object returned by the useOtpField composable.

NameTypeDescription
controlProps
Ref<{ 'aria-invalid': boolean; 'aria-errormessage': string; 'aria-describedby': string; 'aria-label'?: string; 'aria-labelledby'?: string; id: string; role: string; }>
The props of the control element.
descriptionPropsThe props of the description element.
displayErrorDisplay the error message for the field.
errorMessageThe error message for the field.
errorMessageProps
Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }>
The props of the error message element.
errorsThe errors for the field.
fieldSlotsThe slots of the OTP field. Use this as an iterable to render with `v-for`.
fieldValueThe value of the field.
isDirtyWhether the field is dirty.
isDisabledWhether the field is disabled.
isTouchedWhether the field is touched.
isValidWhether the field is valid.
labelPropsThe props of the label element.
setErrors
(messages: Arrayable<string>) => void
Sets the errors for the field.
setTouchedSets the touched state for the field.
setValueSets the value for the field.
submitErrorMessageThe error message for the field from the last submit attempt.
submitErrorsThe errors for the field from the last submit attempt.
validate
(mutate?: boolean) => Promise<ValidationResult<unknown>>
Validates the field.
validityDetailsThe validity details of the OTP field.

useOtpSlot

Props

These are the properties that can be passed to the useOtpSlot composable.

NameTypeDescription
acceptThe type of the slot.
disabledWhether the slot is disabled.
maskedWhether the slot is masked.
readonlyWhether the slot is readonly.
valueThe value of the slot.

Returns

These are the properties in the object returned by the useOtpSlot composable.

NameTypeDescription
key
slotProps
value