Components / Form & Input / Input OTP
Input OTP beta
Six boxes for a code that arrives somewhere else — typed, pasted, or autofilled as one.
<div class="k-otp" data-otp role="group" aria-label="Verification code"> <input type="text" inputmode="numeric" autocomplete="one-time-code" aria-label="Digit 1" value="4"> <input type="text" inputmode="numeric" aria-label="Digit 2" value="8"> <input type="text" inputmode="numeric" aria-label="Digit 3" value="2"> <span class="gap"></span> <input type="text" inputmode="numeric" aria-label="Digit 4"> <input type="text" inputmode="numeric" aria-label="Digit 5"> <input type="text" inputmode="numeric" aria-label="Digit 6"> </div>
.k-otp{display:flex;gap:8px;align-items:center;font-family:var(--k-font)}
.k-otp input{width:44px;height:50px;border:1px solid var(--k-line);border-radius:10px;background:var(--k-bg);color:var(--k-ink);font-family:var(--k-mono);font-size:19px;font-weight:600;text-align:center;outline:none;transition:border-color .13s, box-shadow .13s}
.k-otp input:focus{border-color:var(--k-accent);box-shadow:0 0 0 3px var(--k-accent-bg)}
.k-otp .gap{width:10px;height:2px;border-radius:2px;background:var(--k-line)}
const otpBoxes = [...document.querySelectorAll('[data-otp] input')];
otpBoxes.forEach((box, i) => {
box.addEventListener('input', () => {
box.value = box.value.replace(/\D/g, '').slice(-1);
if (box.value && i < otpBoxes.length - 1) otpBoxes[i + 1].focus();
});
box.addEventListener('keydown', e => {
if (e.key === 'Backspace' && !box.value && i > 0) otpBoxes[i - 1].focus();
if (e.key === 'ArrowLeft' && i > 0) otpBoxes[i - 1].focus();
if (e.key === 'ArrowRight' && i < otpBoxes.length - 1) otpBoxes[i + 1].focus();
});
box.addEventListener('paste', e => {
e.preventDefault();
const text = (e.clipboardData.getData('text') || '').replace(/\D/g, '');
text.split('').slice(0, otpBoxes.length - i).forEach((ch, j) => { otpBoxes[i + j].value = ch; });
otpBoxes[Math.min(i + text.length, otpBoxes.length - 1)].focus();
});
});
Usage
For verification codes and short PINs. One component, one inputmode, no per-box tab stops to fight with.
Accessibility
Each box is a real input with inputmode="numeric" and autocomplete="one-time-code" on the first, so password managers and SMS autofill land properly. Arrow keys and paste are handled.
Related parts
Button stable
Black leads; lines and ghosts support. Hover, disabled and loading states included.
Checkbox / Radio stable
Native inputs with a restyled face. Keyboard support and form behavior stay intact.
Color Picker beta
A curated palette first, any color second — swatches for the 95% case, a native picker for the rest.
Combobox beta
Type to filter, arrows to choose — the Select for lists too long to scan.