OTP Input
The otp input is optimized for entering a sequence of digits or letters. The input fields allow one character at a time. When the digit or letter is entered, focus transfers to the next input in the sequence, until every input is filled.
Features
Install
Install the component from your command line.
Anatomy
Import all parts and piece them together.
<script setup lang="ts">import * as otpInput from '@destyler/otp-input'import { normalizeProps, useMachine } from '@destyler/vue'import { computed, useId } from 'vue'
const [state, send] = useMachine(otpInput.machine({ id: useId(),}))
const api = computed(() => otpInput.connect(state.value, send, normalizeProps))</script>
<template> <div v-bind="api.getRootProps()"> <input v-bind="api.getInputProps({ index: 0 })"> <input v-bind="api.getInputProps({ index: 1 })"> <input v-bind="api.getInputProps({ index: 2 })"> </div> <button @click="api.clearValue" /></template>import * as otpInput from '@destyler/otp-input'import { normalizeProps, useMachine } from '@destyler/react'import { useId, useMemo } from 'react'
export default function OtpInput() { const [state, send] = useMachine(otpInput.machine({ id: useId(), }))
const api = useMemo(() => otpInput.connect(state, send, normalizeProps), [state, send])
return ( <div {...api.getRootProps()} > <input {...api.getInputProps({ index:0 })}/> <input {...api.getInputProps({ index:1 })}/> <input {...api.getInputProps({ index:2 })}/> </div> <button onClick={api.clearValue}></button> )}<script lang="ts"> import * as otpInput from "@destyler/otp-input"; import { normalizeProps, useMachine } from "@destyler/svelte";
const id = $props.id();
const [state, send] = useMachine(otpInput.machine({ id, }));
const api = $derived(otpInput.connect(state, send, normalizeProps));</script>
<div {...api.getRootProps()}> <input {...api.getInputProps({ index:0 })} /> <input {...api.getInputProps({ index:1 })} /> <input {...api.getInputProps({ index:2 })} /></div><button onclick={api.clearValue}></button>import * as otpInput from '@destyler/otp-input'import { normalizeProps, useMachine } from '@destyler/solid'import { createMemo, createUniqueId } from 'solid-js'
export default function OtpInput() { const uniqueId = createUniqueId()
const [state, send] = useMachine(otpInput.machine({ id: uniqueId, }))
const api = createMemo(() => otpInput.connect(state, send, normalizeProps))
return ( <div {...api().getRootProps()}> <input {...api().getInputProps({ index: 0 })}/> <input {...api().getInputProps({ index: 1 })}/> <input {...api().getInputProps({ index: 2 })}/> </div> <button onClick={() => api().clearValue()}></button> )}Setting a default value
To set the initial value of the otp input, pass the value property to the destyler’s context.
const [state, send] = useMachine( otpInput.machine({ defaultValue: ["1", "2", ""], }),)Controlled usage
OTP input value can be uncontrolled or controlled. Prefer defaultValue for an uncontrolled seed. Passing value into machine(...) is presence-controlled — sync with onValueChange and adapter context. See Controllable State.
otpInput.machine({ id: "1", defaultValue: ["1", "2", ""],})
const value = ["1", "2", ""] // your reactive stateotpInput.machine({ id: "1", value, onValueChange(details) { // details => { value: string[] } },})Changing the placeholder
To customize the default otp input placeholder (○) for each input,
pass the placeholder prop and set it to your desired value.
const [state, send] = useMachine( otpInput.machine({ placeholder: "*", }),)Blur on complete
By default, the last input maintains focus when filled and we invoke the onComplete callback.
To blur the last input when the user completes the input, set the blurOnComplete: true in the destyler’s context.
const [state, send] = useMachine( otpInput.machine({ blurOnComplete: true, }),)Allowing alphanumeric values
By default, the otp input accepts only number values but you can choose between numeric,
alphanumeric and alphabetic values. To change the input mode, pass the type
context property and set its value to alphanumeric.
const [state, send] = useMachine( otpInput.machine({ type: "alphanumeric", }),)Using OTP mode
To trigger smartphone OTP auto-suggestion, it is recommended to set the
autocomplete attribute to “one-time-code”. The pin-input machine provides
support for this automatically when you set the otp context property to true.
const [state, send] = useMachine( otpInput.machine({ otp: true, }),)Securing the text input
When collecting private or sensitive information using the otp input,
you might need to mask the value entered, similar to <input type="password"/>.
Pass the mask context property and set it to true.
const [state, send] = useMachine( otpInput.machine({ mask: true, }),)Listening for changes
The otp input machine invokes several callback functions when the user enters:
-
onValueChange— Function invoked when the value is changed. -
onValueComplete— Function invoked when all fields have been completed (by typing or pasting). -
onValueInvalid— Function invoked when an invalid value is entered into the input. An invalid value is any value that doesn’t match the specified “type”.
const [state, send] = useMachine( otpInput.machine({ onValueChange(value) { // value => string[] console.log("value changed to:", value) }, onValueComplete(details) { // details => { value: string[], valueAsString: string } console.log("completed value:", details) }, onValueInvalid(details) { // details => { index: number, value: string } console.log("invalid value:", details) }, }),)Paste routing and migration
Pasted characters start at the input that received the input event. Moving focus in a synchronous input callback or before the next animation frame does not move the paste destination. Empty slots keep their indices, values outside the pasted range stay in place, and only characters beyond the last slot are discarded.
For example, pasting 987 into the third input of ['1', '', '3'] produces
['1', '', '9']. Pasting 9 into the first input of ['1', '2', '3'] produces
['9', '2', '3'].
Paste replaces the origin slot even when its caret is collapsed before or after
the old character. The paste input event’s data supplies the inserted text;
when it is unavailable, the beforeinput binding selects the whole slot so the
browser’s replacement value can be used and validated. Keep both handlers from
getInputProps when writing custom bindings. An input-only integration that
omits both beforeinput and paste data retains the whole-input-value fallback;
that value cannot distinguish pasted text from surrounding old text.
This changes the previous behavior, which used focus at the time deferred work
ran and joined existing values before distributing the paste. If an integration
moved focus to redirect a pending paste or relied on empty slots being collapsed,
update it to paste at the intended input instead. Custom machine integrations
that send INPUT.PASTE directly should include the input’s zero-based index.
If omitted, the focused index is captured when the machine receives the event.
The change request still runs on the next animation frame. Controlled inputs
receive a full slot array through onValueChange; the parent can accept it,
replace it, or retain its current value. Untouched slots use the current value at
that frame. Pending work is still cancelled when its machine run stops.
RTL support
The pin input machine supports RTL writing directions.
To set the dir property in the destyler’s context.
When this attribute is set, we attach a dir attribute to the root part.
const [state, send] = useMachine( otpInput.machine({ dir: "rtl", }),)Styling guide
Earlier, we mentioned that each OTP Input part has a
data-partattribute added to them to select and style them in the DOM.
Completed state
When all values have been filled, we attach a data-complete attribute to the root and input parts.
[data-part="root"][data-complete] { /* styles for when all value has been filled */}
[data-part="input"][data-complete] { /* styles for when all value has been filled */}Invalid state
When an invalid value is entered, we attach a data-invalid attribute to the affected input part.
[data-part="input"][data-invalid] { /* styles for when the input is invalid */}Disabled state
When the pin-input is disabled, we attach a data-disabled attribute to the root and input parts.
[data-part="root"][data-disabled] { /* styles for when the input is disabled */}
[data-part="input"][data-invalid] { /* styles for when the input is disabled */}Methods and Properties
Machine Context
The otp input machine exposes the following context properties:
string string string Partial<{ root: string; hiddenInput: string; label: string; control: string; input: (id: string) => string; }> boolean string boolean boolean boolean boolean boolean string[] string[] "alphanumeric" | "numeric" | "alphabetic" (details: ValueChangeDetails) => void (details: ValueChangeDetails) => void (details: ValueInvalidDetails) => void boolean boolean boolean IntlTranslations "ltr" | "rtl" string () => ShadowRoot | Node | Document Machine API
The otp input api exposes the following methods:
string[] string boolean (value: string[]) => void () => void (index: number, value: string) => void () => void Data Attributes
Root
data-scope data-part data-invalid data-disabled data-complete data-readonly Label
data-scope data-part data-invalid data-disabled data-complete data-readonly Input
data-scope data-part data-disabled data-complete data-invalid Accessibility
Keyboard Interaction
ArrowLeft ArrowRight Backspace Delete Control + V