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
Key | Description |
---|---|
→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
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.
<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:
Name | Type | Description |
---|---|---|
required | boolean | Whether 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:
Value | Description |
---|---|
alphanumeric | Alphanumeric characters are accepted (i.e: English letters and numbers), this is the default value. |
numeric | Only numeric characters are accepted. |
all | All 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.
Name | Type | Description |
---|---|---|
accept | The type of the OTP field characters. | |
description | The description of the OTP field. | |
disabled | Whether the OTP field is disabled. | |
disableHtmlValidation | Whether to disable HTML validation. | |
label | The label of the OTP field. | |
length | The length of the OTP field characters. | |
mask | Whether the OTP field is masked. | |
modelValue | The model value of the OTP field. | |
name | The name of the OTP field. | |
onCompleted | The callback function that is called when the OTP field is completed. | |
prefix | The prefix of the OTP field. If you prefix your codes with a character, you can set it here (e.g "G-"). | |
readonly | Whether the OTP field is readonly. | |
required | Whether the OTP field is required. | |
schema | Schema for field validation. | |
value | The initial value of the OTP field. |
Returns
These are the properties in the object returned by the useOtpField
composable.
Name | Type | Description |
---|---|---|
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. |
descriptionProps | The props of the description element. | |
displayError | Display the error message for the field. | |
errorMessage | The error message for the field. | |
errorMessageProps | Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }> | The props of the error message element. |
errors | The errors for the field. | |
fieldSlots | The slots of the OTP field. Use this as an iterable to render with `v-for`. | |
fieldValue | The value of the field. | |
isDirty | Whether the field is dirty. | |
isDisabled | Whether the field is disabled. | |
isTouched | Whether the field is touched. | |
isValid | Whether the field is valid. | |
labelProps | The props of the label element. | |
setErrors | (messages: Arrayable<string>) => void | Sets the errors for the field. |
setTouched | Sets the touched state for the field. | |
setValue | Sets the value for the field. | |
submitErrorMessage | The error message for the field from the last submit attempt. | |
submitErrors | The errors for the field from the last submit attempt. | |
validate | (mutate?: boolean) => Promise<ValidationResult<unknown>> | Validates the field. |
validityDetails | The validity details of the OTP field. |
useOtpSlot
Props
These are the properties that can be passed to the useOtpSlot
composable.
Name | Type | Description |
---|---|---|
accept | The type of the slot. | |
disabled | Whether the slot is disabled. | |
masked | Whether the slot is masked. | |
readonly | Whether the slot is readonly. | |
value | The value of the slot. |
Returns
These are the properties in the object returned by the useOtpSlot
composable.
Name | Type | Description |
---|---|---|
key | ||
slotProps | ||
value |