Two-Factor Authentication Components
Secure verification code input components for two-factor authentication. Perfect for OTP verification, SMS codes, and security confirmation.
Installation
1. Stimulus Controller Setup
Start by adding the following controller to your project:
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["num1", "num2", "num3", "num4", "num5", "num6", "form", "submitButton"];
static values = {
autoSubmit: { type: Boolean, default: false }, // Whether to automatically submit the form
autofocus: { type: Boolean, default: true }, // Whether to autofocus the first input
};
connect() {
// Set autofocus on the first input if it's a target and autofocus is enabled
if (this.autofocusValue && this.hasNum1Target) {
this.num1Target.focus();
}
// Add focus event listeners to all input targets
this._getInputTargets().forEach((input) => {
input.addEventListener("focus", this.handleFocus.bind(this));
});
}
disconnect() {
// Clean up event listeners
this._getInputTargets().forEach((input) => {
input.removeEventListener("focus", this.handleFocus.bind(this));
});
}
handleFocus(event) {
const input = event.target;
// Move cursor to end of input value
setTimeout(() => {
input.setSelectionRange(input.value.length, input.value.length);
}, 0);
}
isNumber(value) {
return /^[0-9]$/.test(value);
}
handleInput(event) {
const currentInput = event.target;
const nextInput = this._getNextInput(currentInput);
if (this.isNumber(currentInput.value)) {
if (nextInput) {
nextInput.focus();
} else {
// Last input filled
this.handleSubmit();
}
} else {
currentInput.value = "";
}
}
handleKeydown(event) {
const currentInput = event.target;
if (event.key === "Backspace" && currentInput.value === "") {
const prevInput = this._getPreviousInput(currentInput);
if (prevInput) {
prevInput.focus();
}
}
}
handlePaste(event) {
event.preventDefault();
const paste = (event.clipboardData || window.clipboardData).getData("text").trim();
if (paste.length === 6 && /^[0-9]+$/.test(paste)) {
this.num1Target.value = paste.charAt(0);
this.num2Target.value = paste.charAt(1);
this.num3Target.value = paste.charAt(2);
this.num4Target.value = paste.charAt(3);
this.num5Target.value = paste.charAt(4);
this.num6Target.value = paste.charAt(5);
this.handleSubmit();
this.num6Target.focus(); // Focus last input after paste, then handleSubmit will move to button
}
}
handleSubmit(event) {
if (event) {
// If triggered by form submit event
event.preventDefault();
}
if (this.hasSubmitButtonTarget) {
this.submitButtonTarget.focus();
}
// Submit form if autoSubmit is true
if (this.autoSubmitValue) {
if (this.hasFormTarget) {
this.formTarget.submit();
}
}
}
_getInputTargets() {
return [
this.num1Target,
this.num2Target,
this.num3Target,
this.num4Target,
this.num5Target,
this.num6Target,
].filter((target) => target); // Filter out any potentially missing targets
}
_getNextInput(currentInput) {
const inputs = this._getInputTargets();
const currentIndex = inputs.indexOf(currentInput);
if (currentIndex !== -1 && currentIndex < inputs.length - 1) {
return inputs[currentIndex + 1];
}
return null;
}
_getPreviousInput(currentInput) {
const inputs = this._getInputTargets();
const currentIndex = inputs.indexOf(currentInput);
if (currentIndex > 0) {
return inputs[currentIndex - 1];
}
return null;
}
}
Examples
Basic Two-Factor Authentication
A complete two-factor authentication form with 6-digit verification code input.
Two-Factor Authentication
Confirm your account by entering the verification code sent to your phone number ending in **432.
Haven't received it?
Get a new code
<div class="mx-auto w-full max-w-xl py-6" data-controller="two-factor">
<div class="rounded-2xl border bg-white dark:bg-neutral-900 border-neutral-200 text-center dark:border-neutral-700">
<div class="p-5 sm:p-8 md:p-12">
<svg xmlns="http://www.w3.org/2000/svg" class="size-6 mx-auto" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><line x1="9" y1="11" x2="9" y2="12"></line><path d="M6.25,7.628v-3.128c0-1.519,1.231-2.75,2.75-2.75h0c1.519,0,2.75,1.231,2.75,2.75v3.129"></path><circle cx="9" cy="11.5" r="4.75"></circle></g></svg>
<h1 class="my-2 text-2xl font-bold">Two-Factor Authentication</h1>
<h2 class="mb-8 text-sm text-neutral-600 dark:text-neutral-400">
Confirm your account by entering the verification code sent to
your phone number ending in **432.
</h2>
<form
data-two-factor-target="form"
data-action="submit->two-factor#handleSubmit"
class="space-y-6">
<div class="inline-flex items-center gap-1.5">
<input
data-two-factor-target="num1"
data-action="input->two-factor#handleInput paste->two-factor#handlePaste"
type="text"
inputmode="numeric"
id="num1"
name="num1"
maxlength="1"
autocomplete="one-time-code"
required
class="block w-8 rounded-lg border border-neutral-200 dark:border-neutral-600 px-2 py-1 text-center text-base placeholder-neutral-500 focus:outline-neutral-800 focus:border-neutral-800 dark:focus:outline-neutral-50 dark:focus:border-neutral-50 dark:bg-neutral-800 dark:placeholder-neutral-400">
<input
data-two-factor-target="num2"
data-action="input->two-factor#handleInput keydown->two-factor#handleKeydown paste->two-factor#handlePaste"
type="text"
inputmode="numeric"
id="num2"
name="num2"
maxlength="1"
autocomplete="off"
required
class="block w-8 rounded-lg border border-neutral-200 dark:border-neutral-600 px-2 py-1 text-center text-base placeholder-neutral-500 focus:outline-neutral-800 focus:border-neutral-800 dark:focus:outline-neutral-50 dark:focus:border-neutral-50 dark:bg-neutral-800 dark:placeholder-neutral-400">
<input
data-two-factor-target="num3"
data-action="input->two-factor#handleInput keydown->two-factor#handleKeydown paste->two-factor#handlePaste"
type="text"
inputmode="numeric"
id="num3"
name="num3"
maxlength="1"
autocomplete="off"
required
class="block w-8 rounded-lg border border-neutral-200 dark:border-neutral-600 px-2 py-1 text-center text-base placeholder-neutral-500 focus:outline-neutral-800 focus:border-neutral-800 dark:focus:outline-neutral-50 dark:focus:border-neutral-50 dark:bg-neutral-800 dark:placeholder-neutral-400">
<span class="text-sm text-neutral-400 dark:text-neutral-600">-</span>
<input
data-two-factor-target="num4"
data-action="input->two-factor#handleInput keydown->two-factor#handleKeydown paste->two-factor#handlePaste"
type="text"
inputmode="numeric"
id="num4"
name="num4"
maxlength="1"
autocomplete="off"
required
class="block w-8 rounded-lg border border-neutral-200 dark:border-neutral-600 px-2 py-1 text-center text-base placeholder-neutral-500 focus:outline-neutral-800 focus:border-neutral-800 dark:focus:outline-neutral-50 dark:focus:border-neutral-50 dark:bg-neutral-800 dark:placeholder-neutral-400">
<input
data-two-factor-target="num5"
data-action="input->two-factor#handleInput keydown->two-factor#handleKeydown paste->two-factor#handlePaste"
type="text"
inputmode="numeric"
id="num5"
name="num5"
maxlength="1"
autocomplete="off"
required
class="block w-8 rounded-lg border border-neutral-200 dark:border-neutral-600 px-2 py-1 text-center text-base placeholder-neutral-500 focus:outline-neutral-800 focus:border-neutral-800 dark:focus:outline-neutral-50 dark:focus:border-neutral-50 dark:bg-neutral-800 dark:placeholder-neutral-400">
<input
data-two-factor-target="num6"
data-action="input->two-factor#handleInput keydown->two-factor#handleKeydown paste->two-factor#handlePaste"
type="text"
inputmode="numeric"
id="num6"
name="num6"
maxlength="1"
autocomplete="off"
required
class="block w-8 rounded-lg border border-neutral-200 dark:border-neutral-600 px-2 py-1 text-center text-base placeholder-neutral-500 focus:outline-neutral-800 focus:border-neutral-800 dark:focus:outline-neutral-50 dark:focus:border-neutral-50 dark:bg-neutral-800 dark:placeholder-neutral-400">
</div>
<div class="flex justify-center">
<button
data-two-factor-target="submitButton"
type="submit"
class="min-w-32 flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200">
<span>Verify code</span>
</button>
</div>
</form>
<div class="mt-5 text-sm text-neutral-500 dark:text-neutral-400">
Haven't received it?
<a href="#" class="font-medium text-neutral-700 underline decoration-neutral-500/50 underline-offset-2 hover:text-neutral-900 dark:text-neutral-300 dark:decoration-neutral-400/50 dark:hover:text-neutral-100">
Get a new code
</a>
</div>
</div>
</div>
</div>
Configuration
The two-factor authentication component is powered by a Stimulus controller that provides automatic focus management, input validation, paste support, and auto-submit functionality.
Controller Setup
Basic two-factor authentication structure with required data attributes:
<div data-controller="two-factor">
<form data-two-factor-target="form" data-action="submit->two-factor#handleSubmit">
<input data-two-factor-target="num1" data-action="input->two-factor#handleInput paste->two-factor#handlePaste" type="text" inputmode="numeric" maxlength="1" autocomplete="one-time-code">
<input data-two-factor-target="num2" data-action="input->two-factor#handleInput keydown->two-factor#handleKeydown paste->two-factor#handlePaste" type="text" inputmode="numeric" maxlength="1" autocomplete="off">
<input data-two-factor-target="num3" data-action="input->two-factor#handleInput keydown->two-factor#handleKeydown paste->two-factor#handlePaste" type="text" inputmode="numeric" maxlength="1" autocomplete="off">
<input data-two-factor-target="num4" data-action="input->two-factor#handleInput keydown->two-factor#handleKeydown paste->two-factor#handlePaste" type="text" inputmode="numeric" maxlength="1" autocomplete="off">
<input data-two-factor-target="num5" data-action="input->two-factor#handleInput keydown->two-factor#handleKeydown paste->two-factor#handlePaste" type="text" inputmode="numeric" maxlength="1" autocomplete="off">
<input data-two-factor-target="num6" data-action="input->two-factor#handleInput keydown->two-factor#handleKeydown paste->two-factor#handlePaste" type="text" inputmode="numeric" maxlength="1" autocomplete="off">
<button data-two-factor-target="submitButton" type="submit">Verify</button>
</form>
</div>
Configuration Values
Prop | Description | Type | Default |
---|---|---|---|
autoSubmit
|
Automatically submits the form when all 6 digits are entered |
Boolean
|
false
|
autofocus
|
Automatically focuses the first input field when the component connects |
Boolean
|
true
|
Targets
Target | Description | Required |
---|---|---|
num1
|
First digit input field | Required |
num2
|
Second digit input field | Required |
num3
|
Third digit input field | Required |
num4
|
Fourth digit input field | Required |
num5
|
Fifth digit input field | Required |
num6
|
Sixth digit input field | Required |
form
|
The form element containing all input fields | Required |
submitButton
|
The submit button that gets focused after all inputs are filled | Required |
Actions
Action | Description | Usage |
---|---|---|
handleInput
|
Handles input validation and automatic navigation to the next field |
data-action="input->two-factor#handleInput"
|
handleKeydown
|
Handles backspace navigation to previous fields |
data-action="keydown->two-factor#handleKeydown"
|
handlePaste
|
Handles pasting of 6-digit codes and auto-filling all fields |
data-action="paste->two-factor#handlePaste"
|
handleSubmit
|
Handles form submission and focus management |
data-action="submit->two-factor#handleSubmit"
|
Key Features
- Automatic Navigation: Moves focus to the next input when a digit is entered
- Backspace Navigation: Moves focus to the previous input when backspace is pressed on an empty field
- Paste Support: Automatically fills all fields when a 6-digit code is pasted
- Input Validation: Only accepts numeric input (0-9)
- Auto Submit: Optional automatic form submission when all digits are entered
- Focus Management: Automatic focusing of first input and submit button
Accessibility Features
-
Mobile Optimized: Uses
inputmode="numeric"
to show numeric keyboard on mobile -
Autocomplete Support: First input uses
autocomplete="one-time-code"
for better UX - Screen Reader Friendly: Proper form labels and input attributes for accessibility
- Keyboard Navigation: Full keyboard support with backspace navigation
Usage Notes
-
Each input should have
maxlength="1"
to limit to single digits -
Use
type="text"
withinputmode="numeric"
for better mobile experience -
The first input should have
autocomplete="one-time-code"
, others should useautocomplete="off"
-
Only the first input needs the
paste
action - it will handle pasting for all fields