PIN Input
<input maxlength="1"> × N
A row of individual character inputs for entering verification codes, PINs, and OTPs. Auto-advances on entry, goes back on Backspace, and supports paste.
Overview
Preview
Enter the 6-digit code sent to your email.
Basic 6-digit
vue
<script setup>
import { ref, computed } from 'vue'
const inputs = ref([])
const pin = ref(Array(6).fill(''))
const isComplete = computed(() => pin.value.every(v => v !== ''))
function onInput(i, e) {
const val = e.target.value.replace(/\D/g, '') // digits only
pin.value[i] = val.slice(-1)
if (val && i < 5) inputs.value[i + 1]?.focus() // advance
}
function onKeydown(i, e) {
if (e.key === 'Backspace' && !pin.value[i] && i > 0) {
inputs.value[i - 1]?.focus() // retreat
}
if (e.key === 'ArrowLeft' && i > 0) inputs.value[i - 1]?.focus()
if (e.key === 'ArrowRight' && i < 5) inputs.value[i + 1]?.focus()
}
function onPaste(e) {
const text = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 6)
text.split('').forEach((c, i) => { if (i < 6) pin.value[i] = c })
inputs.value[Math.min(text.length, 5)]?.focus()
e.preventDefault()
}
</script>
<template>
<div class="flex gap-2" role="group" aria-label="6-digit verification code">
<input
v-for="(_, i) in pin"
:key="i"
:ref="el => { if (el) inputs[i] = el }"
v-model="pin[i]"
type="text"
inputmode="numeric"
maxlength="1"
class="h-12 w-11 rounded-lg border bg-zinc-900 text-center text-lg font-semibold
text-zinc-100 caret-transparent outline-none transition-all"
:class="pin[i]
? 'border-[#D40C37] ring-2 ring-[#D40C37]/15'
: 'border-zinc-700 focus:border-[#D40C37] focus:ring-2 focus:ring-[#D40C37]/15'"
@input="onInput(i, $event)"
@keydown="onKeydown(i, $event)"
@paste="onPaste"
@focus="$event.target.select()"
/>
</div>
<p class="mt-2 text-xs text-emerald-400" v-if="isComplete">
Code entered: {{ pin.join('') }}
</p>
</template>Sizes
html
<!-- Small (4-digit PIN) -->
<input class="h-10 w-10 text-base ..." />
<!-- Default (6-digit code) -->
<input class="h-12 w-11 text-lg ..." />
<!-- Large -->
<input class="h-14 w-12 text-xl ..." />Masked PIN (password type)
html
<input
type="password"
inputmode="numeric"
maxlength="1"
class="h-12 w-11 rounded-lg border border-zinc-700 bg-zinc-900 text-center text-lg
text-zinc-100 caret-transparent outline-none focus:border-[#D40C37]"
...
/>Error state
html
<input
class="h-12 w-11 rounded-lg border border-red-500 bg-zinc-900 text-center
text-lg text-zinc-100 ring-2 ring-red-500/15 outline-none ..."
/>html
<p class="mt-2 text-xs text-red-400" role="alert">
Invalid code. Please try again.
</p>4-digit variant
vue
<script setup>
const pin = ref(Array(4).fill(''))
// same onInput / onKeydown — just change bounds from 5 to 3
</script>Accessibility notes
- Wrap the inputs in a
<div role="group" aria-label="Verification code">so screen readers announce the group - Use
inputmode="numeric"to show the numeric keyboard on mobile caret-transparenthides the cursor — the single-char box makes the caret position obvious anyway@focus="$event.target.select()"selects any existing character so typing immediately replaces it- After successful submission, announce the result with
role="status"oraria-live="polite"