Formsform
OTP Input
Split-cell OTP/2FA code input with automatic focus forwarding.
Live Preview
Enter your verification code
Usage
Copy and paste this code into your React component:
tsx
import { OTPInput } from 'react-component-kit-ui/forms';
<OTPInput />Manual Installation
If you are not using the npm package, create a new file and paste the following code: src/components/OTPInput.tsx + OTPInput.css:
OTPInput.tsx
'use client';
import { useEffect, useRef } from 'react';
import './OTPInput.css';
/**
* OTPInput โ A specialized input group for One-Time Password verification codes.
*
* @param {string} label - Optional grouping label title.
* @param {string} id - Base HTML ID for the inputs.
* @param {number} length - Number of OTP digits. Default is 4.
* @param {string} name - Base name for form submission (will be indexed).
*/
export interface OTPInputProps {
label?: string;
id: string;
length?: number;
name?: string;
}
export function OTPInput({ label, id, length = 4, name = "otp" }: OTPInputProps) {
// Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
const mounted = useRef(false);
useEffect(() => {
if (mounted.current) return;
mounted.current = true;
document.querySelectorAll('[data-otp-group]').forEach(group => {
const inputs = group.querySelectorAll('input') as NodeListOf<HTMLInputElement>;
inputs.forEach((input, idx) => {
// Auto-advance
input.addEventListener('input', (e) => {
const val = (e.target as HTMLInputElement).value;
if (val && idx < inputs.length - 1) {
inputs[idx + 1].focus();
}
});
// Backspace logic
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !input.value && idx > 0) {
inputs[idx - 1].focus();
}
});
// Paste logic
input.addEventListener('paste', (e) => {
e.preventDefault();
const data = e.clipboardData?.getData('text').slice(0, inputs.length).split('') || [];
data.forEach((val, i) => {
if (inputs[i]) inputs[i].value = val;
});
if (inputs[data.length - 1]) inputs[data.length - 1].focus();
});
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<div className="otp-container" data-rk-a692f6="">
{label && <label className="otp-label" data-rk-a692f6="">{label}</label>}
<div className="otp-wrap" data-otp-group data-rk-a692f6="">
{Array.from({ length }).map((_, i) => (
<input key={i} type="text" inputMode="numeric" maxLength={1} pattern="[0-9]*" autoComplete="one-time-code" id={`${id}-${i}`} name={`${name}[]`} className="otp-field" data-otp-index={i} data-rk-a692f6="" />
))}
</div>
</div>
</>
);
}
export default OTPInput;OTPInput.css
/* OTPInput โ ported from astro-component-kit (forms/OTPInput.astro) */
.otp-container:where([data-rk-a692f6]) { display: flex; flex-direction: column; align-items: center; gap: var(--sp-4, 1rem); }
.otp-label:where([data-rk-a692f6]) { font-size: 0.9rem; font-weight: 600; color: var(--c-text-2, #94a3b8); }
.otp-wrap:where([data-rk-a692f6]) { display: flex; gap: 0.75rem; justify-content: center; }
.otp-field:where([data-rk-a692f6]) {
width: 50px; height: 60px;
background: var(--c-bg-elev, rgba(255,255,255,0.05));
border: 1px solid var(--c-border, rgba(255,255,255,0.1));
border-radius: var(--r-md, 12px);
text-align: center; color: var(--c-text-1, #fff);
font-size: 1.75rem; font-weight: 800;
outline: none; transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.otp-field:focus:where([data-rk-a692f6]) {
border-color: var(--c-primary, #6366f1);
background: rgba(99, 102, 241, 0.1);
box-shadow: 0 0 15px rgba(99, 102, 241, 0.3), 0 0 0 3px rgba(99, 102, 241, 0.15);
transform: translateY(-2px);
}Quick Info
- Category
- Forms
- Filename
OTPInput.tsx- Dependencies
- None โ pure React + CSS
- Tags
- form