Guide
Getting started
npm install @devix-labs/otp-input
<label for="code">Verification code</label>
<input id="code" name="code">
import { createOtpInput } from '@devix-labs/otp-input';
import '@devix-labs/otp-input/styles.css';
const otp = createOtpInput(document.querySelector('#code'), {
length: 6,
autoFocus: true,
webOtp: true,
onComplete: (code) => form.submit(),
});
The input you point at becomes the hidden field that carries the whole code, so a plain form posts one value under its existing name. Everything else is drawn beside it.
Autofill
Three things have to be true for a code to arrive by itself, and this does all three:
| Platform | What it needs | What we do |
|---|---|---|
| iOS, Safari and password managers | autocomplete="one-time-code" |
on the first box |
| Chrome on Android | the WebOTP API | webOtp: true asks for it |
| Dashlane, 1Password | a real input they can fill | the boxes are real inputs |
When a manager drops all six characters into the first box — which is what they do — the code is spread across the rest instead of being truncated.
For WebOTP, your SMS has to end with the origin line:
Your code is 123456
@example.com #123456
What may be typed
createOtpInput(input, { length: 6, mode: 'numeric' }); // the default
createOtpInput(input, { length: 8, mode: 'alphanumeric' }); // folded to upper case
createOtpInput(input, { length: 4, allow: (c) => /[0-9a-f]/i.test(c) }); // your own rule
Arabic-Indic, Persian, Devanagari, Bengali and full-width digits are read as digits, so a code pasted from an Arabic SMS works.
Looks
createOtpInput(input, { length: 6, separators: [2], separator: '–' }); // 123–456
createOtpInput(input, { length: 4, mask: true }); // a PIN
Separators are decoration: they never appear in the value.
After a wrong code
const otp = createOtpInput(input, { length: 6, onComplete: check });
async function check(code) {
const ok = await verify(code);
if (!ok) otp.reject('That code has expired — we have sent a new one');
}
reject() marks every box, announces the message to a screen reader, and puts the cursor back at the
start.
The rest of the API
| Call | Does |
|---|---|
getValue() / setValue(code) |
|
clear() |
Empties it and focuses the first box. |
focus(index?) |
Without an index, the first empty box. |
reject(message?) |
Marks a failed attempt. |
enable() / disable() |
|
destroy() |
Puts the original input back exactly as it was. |
Events dx:otpchange and dx:otpcomplete bubble from the group, and the hidden input fires input
and change, so a framework binding on it works without any of this API.
React
import { OtpField } from '@devix-labs/otp-input/react';
<OtpField length={6} webOtp autoFocus onComplete={(code) => verify(code)} />
It renders as a plain input on the server and upgrades in the browser, so there is no hydration warning.
Vue, Svelte and <dx-otp length="6" name="code" web-otp> are in the package too.