Skip to content

Instantly share code, notes, and snippets.

@andreasvirkus
Created September 1, 2025 19:24
Show Gist options
  • Save andreasvirkus/9f0ab367e688e5cc1058e97473584efb to your computer and use it in GitHub Desktop.
Save andreasvirkus/9f0ab367e688e5cc1058e97473584efb to your computer and use it in GitHub Desktop.

Revisions

  1. andreasvirkus created this gist Sep 1, 2025.
    80 changes: 80 additions & 0 deletions OtpInput.vue
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,80 @@
    <template>
    <div class="otp-input my-4 flex justify-center gap-2 text-black">
    <input
    v-for="i of 6"
    :key="i"
    v-model="otp[i - 1]"
    type="tel"
    maxlength="1"
    class="otp-field text-heading"
    required
    @input="($event) => handleInput($event, i - 1)"
    @paste="handlePaste"
    @keydown.enter="handleSubmit"
    @keydown.right="focusNext(i - 1)"
    @keydown.left="focusPrev(i - 1)"
    @keydown.delete="focusPrev(i - 1, true)"
    />
    </div>
    </template>

    <script setup lang="ts">
    const emit = defineEmits<{
    (event: 'update', otp: string): void
    }>()
    const otp = ref(['', '', '', '', '', ''])
    const otpToken = computed(() => otp.value.join(''))
    watch(otpToken, (val) => {
    if (val.length === 6) emit('update', val)
    })
    const handleInput = (event: Event, i: number) => {
    const val = (event.target as HTMLInputElement).value
    if (isNaN(Number(val))) {
    otp.value[i] = ''
    return
    }
    if ((event.target as HTMLInputElement).value) focusNext(i)
    }
    const focusNext = (index: number) => {
    const nextInput = document.querySelectorAll('.otp-field')[index + 1]
    ;(nextInput as HTMLElement)?.focus()
    }
    const focusPrev = (index: number, clearField = false) => {
    if (clearField && !!otp.value[index]) {
    otp.value[index] = ''
    return
    }
    const prevInput = document.querySelectorAll('.otp-field')[index - 1]
    ;(prevInput as HTMLElement)?.focus()
    }
    const handlePaste = (event: ClipboardEvent) => {
    const pastedData = event.clipboardData?.getData('text')?.trim() ?? ''
    if (pastedData.length === 6) {
    otp.value = pastedData.split('')
    // Focus the last input field after pasting
    ;(document.querySelector('.otp-field:last-child') as HTMLElement)?.focus()
    }
    }
    const handleSubmit = () => {
    if (otpToken.value.length < otp.value.length) return
    emit('update', otpToken.value)
    }
    </script>

    <style>
    .otp-field {
    width: 40px;
    height: 70px;
    border-radius: 8px;
    border: 2px solid #ccc;
    text-align: center;
    font-size: 40px;
    }
    </style>