Github

Input OTP

N adjacent <input class="otp-slot"> in .otp. Digits only. A digit moves focus forward; Backspace on an empty slot moves back. role="group" names the cluster via aria-labelledby; each slot has its own aria-label. These are real inputs, not one fake field. There is no wrapper.

Preview

One-time code

Code

<script>
	function otp_event(event) {
		const input = event.target;
		if (!input.classList.contains('otp-slot')) return;
		const slots = [...input.closest('.otp').querySelectorAll('.otp-slot')];
		const i = slots.indexOf(input);

		if (event.type === 'beforeinput') {
			if (event.inputType.startsWith('insert') && !(event.data && /^\d+$/.test(event.data)))
				event.preventDefault();
			return;
		}

		if (event.type === 'input') {
			const digit = input.value.replace(/\D/g, '').slice(-1);
			input.value = digit;
			if (digit) slots[i + 1]?.focus();
			return;
		}

		if (event.type === 'keydown' && event.key === 'Backspace' && !input.value && i > 0) {
			event.preventDefault();
			slots[i - 1].focus();
			return;
		}

		if (event.type === 'focus')
			input.select();
	}
</script>

<div class="field">
	<div class="label" id="otp-label">One-time code</div>
	<div class="otp" role="group" aria-labelledby="otp-label">
		<input class="otp-slot" type="text" inputmode="numeric" maxlength="1" spellcheck="false" autocapitalize="off" pattern="[0-9]" onbeforeinput={otp_event} oninput={otp_event} onkeydown={otp_event} onfocus={otp_event} autocomplete="one-time-code" aria-label="Digit 1 of 6">
		<input class="otp-slot" type="text" inputmode="numeric" maxlength="1" spellcheck="false" autocapitalize="off" pattern="[0-9]" onbeforeinput={otp_event} oninput={otp_event} onkeydown={otp_event} onfocus={otp_event} aria-label="Digit 2 of 6">
		<input class="otp-slot" type="text" inputmode="numeric" maxlength="1" spellcheck="false" autocapitalize="off" pattern="[0-9]" onbeforeinput={otp_event} oninput={otp_event} onkeydown={otp_event} onfocus={otp_event} aria-label="Digit 3 of 6">
		<input class="otp-slot" type="text" inputmode="numeric" maxlength="1" spellcheck="false" autocapitalize="off" pattern="[0-9]" onbeforeinput={otp_event} oninput={otp_event} onkeydown={otp_event} onfocus={otp_event} aria-label="Digit 4 of 6">
		<input class="otp-slot" type="text" inputmode="numeric" maxlength="1" spellcheck="false" autocapitalize="off" pattern="[0-9]" onbeforeinput={otp_event} oninput={otp_event} onkeydown={otp_event} onfocus={otp_event} aria-label="Digit 5 of 6">
		<input class="otp-slot" type="text" inputmode="numeric" maxlength="1" spellcheck="false" autocapitalize="off" pattern="[0-9]" onbeforeinput={otp_event} oninput={otp_event} onkeydown={otp_event} onfocus={otp_event} aria-label="Digit 6 of 6">
	</div>
</div>