OTP input

Fixed-length one-time code input that keeps digits only. Defaults to length={6}, inputMode="numeric", and autocomplete="one-time-code". Adapters use a single native input; multi-box UIs compose on top of createOtpInputController (setCharAt, applyPaste, clear).

PreviewOTP cells

Usage

tsx
import { useState } from "react";
import { OtpInput } from "@sometic/react/input";

export function Example() {
    const [code, setCode] = useState("");
    return (
        <OtpInput
            length={6}
            value={code}
            onValueChange={(next) => {
                setCode(next);
                if (next.length === 6) {
                    // submit / verify
                }
            }}
        />
    );
}
tsx
import { useState } from "react";
import { OtpInput } from "@sometic/react/input";

export function Example(): JSX.Element {
    const [code, setCode] = useState("");
    return (
        <OtpInput
            length={6}
            value={code}
            onValueChange={(next) => {
                setCode(next);
                if (next.length === 6) {
                    // submit / verify
                }
            }}
        />
    );
}
html
<script type="module">
    import { registerInputElements } from "@sometic/elements/input";
    registerInputElements();
</script>

<sometic-otp-input length="6"></sometic-otp-input>

Vue

vue
<script setup>
import { ref, watch } from "vue";
import { OtpInput } from "@sometic/vue/input";

const code = ref("");
watch(code, (next) => {
    if (next.length === 6) {
        // submit / verify
    }
});
</script>

<template>
    <OtpInput v-model="code" :length="6" />
</template>

There is no separate onComplete prop on React/Vue. Detect completion when value.length === length inside onValueChange / a watch.

How it works

  1. Engine (@sometic/dom/input-otp): resolveOtpInput slices to length (default 6), forces type: "text", defaults inputMode to "numeric" and autocomplete to "one-time-code". createOtpInputController sanitizes with /\D/g, supports setCharAt, applyPaste, and clear for multi-box compositions.
  2. Adapters: React/Vue keep one string value, strip non-digits, and enforce maxLength / maxlength. Vue length defaults to 6.
  3. Custom element: sometic-otp-input observes value, length, disabled, readonly, shadow, and dispatches value-change with { value } (digits only).

Anatomy

Same interactive model as a single Input; value is one string of length ≤ length. Multi-box layouts are your composition using the DOM controller helpers, not separate shipped box components.

Props / attributes

React OtpInputProps

Omit<InputProps, "type" | "value" | "defaultValue"> plus:

PropTypeDefaultDescription
lengthnumber6Max digits
valuestring,Controlled digits string
defaultValuestring,Uncontrolled initial
onValueChange(value: string) => void,Digits only
disabled / readonly / invalidboolean,State + ARIA
namestring,Form association
autocompletestringengine default one-time-codeOverride if needed
styling propsfrom Input,unstyled, classes, …
Native attrsremaining input HTML attrs,Forwarded; ref supported

Vue

modelValue, length (default 6), disabled, readonly. Emits update:modelValue.

Custom element (sometic-otp-input)

Observed: value, length, disabled, readonly, shadow. Event: value-change{ value: string }.

Events / callbacks

SurfaceEventPayload
ReactonValueChangedigits string
Vueupdate:modelValuedigits string
Custom elementvalue-change{ value }

Completion is app-defined (value.length === length). Changes ignored while disabled or readonly.

Controlled vs uncontrolled

Same as Input value contract: controlled value + handler, or defaultValue. Prefer controlled when you verify on completion.

Form participation

Single named input posts the code string. Keep autocomplete="one-time-code" for SMS autofill. Prefer Form field values for verification requests instead of scraping the DOM after submit.

Accessibility

  • One field is the recommended default for AT and OS OTP autofill.
  • Multi-box UIs must keep a cohesive accessible name (one legend/label for the group) and manage focus movement yourself when composing with setCharAt.
  • Announce verification failures via Field / Alert live regions.
  • Keyboard: native text editing; non-digits never stick in the value.

Styling

Input state attrs. Multi-box layouts style your own cells; the shipped adapter is one control:

tsx
<OtpInput length={6} value={code} onValueChange={setCode} unstyled classes={{ root: "otp" }} />

Edge cases

  • Non-digits stripped on every change.
  • Paste longer than length, truncated by sanitize (slice(0, length)).
  • Length changes, prefer a fixed length for a given flow; changing mid-entry truncates via resolve/slice.
  • Alphabet codes, current engine focuses on digits; validate upstream if you need alphanumeric OTPs.
  • SSR, engine is pure; register CE in the browser.

Performance notes

Single string state is cheap. Multi-box UIs should share one createOtpInputController instead of N independent strings.

When to use / When not

Use for MFA, email, and SMS one-time codes.

Do not use for:

FAQ

One box or many? The shipped adapters are one input holding one string. Multi-box is composition on createOtpInputController.

Is there onComplete? No. Derive completion from onValueChange when value.length === length.

Alphabet codes? Digits only today. Filter/validate upstream if you need letters.

Default length? 6 in resolve and Vue/React defaults.

Autocomplete? Defaults to one-time-code. Keep it for SMS autofill unless your platform requires a different token.

Does React forward refs? Yes, to the underlying <input>.

Vue v-model type? string (digits).

Can length be 4 or 8? Yes; pass length.

Light DOM or shadow? Light DOM default; shadow on the CE for isolation.

Bundle tip? Import from @sometic/react/input or @sometic/dom/input-otp.