Untitled.mov
One-time passcode Input. Accessible & unstyled. Based on the React version by guilhermerodz.
npm install vue-input-otp
<script setup lang="ts">
import { OTPInput } from 'vue-input-otp'
import Slot from './Slot.vue'
const input = ref('123456')
</script>
<template>
<form>
<OTPInput v-slot="{ slots }" v-model="input" :maxlength="6">
<!-- slots -->
</OTPInput>
</form>
</template>
The example below uses tailwindcss
, shadcn-vue
, tailwind-merge
and clsx
:
<script setup lang="ts">
import { OTPInput } from 'vue-input-otp'
import Slot from './Slot.vue'
</script>
<template>
<OTPInput
v-slot="{ slots }"
:maxlength="6"
container-class="group flex items-center has-[:disabled]:opacity-30"
>
<div class="flex">
<Slot v-for="(slot, idx) in slots.slice(0, 3)" v-bind="slot" :key="idx" />
</div>
<!-- Fake Dash. Inspired by Stripe's MFA input. -->
<div class="flex w-10 justify-center items-center">
<div class="w-3 h-1 rounded-full bg-border" />
</div>
<div class="flex">
<Slot v-for="(slot, idx) in slots.slice(3)" v-bind="slot" :key="idx" />
</div>
</OTPInput>
</template>
<script setup lang="ts">
import type { SlotProps } from 'vue-input-otp'
import { cn } from '$lib/utils'
defineProps<SlotProps>()
</script>
<template>
<div
:class="cn(
'relative w-10 h-14 text-[2rem]',
'flex items-center justify-center',
'transition-all duration-300',
'border-border border-y border-r first:border-l first:rounded-l-md last:rounded-r-md',
'group-hover:border-accent-foreground/20 group-focus-within:border-accent-foreground/20',
'outline outline-0 outline-accent-foreground/20',
{ 'outline-4 outline-accent-foreground': isActive },
)"
>
<div v-if="char !== null">
{{ char }}
</div>
<!-- Emulate a Fake Caret -->
<div v-if="char === null && isActive" class="absolute pointer-events-none inset-0 flex items-center justify-center animate-caret-blink">
<div class="w-px h-8 bg-white" />
</div>
</div>
</template>
// tailwind.config.ts for the blinking caret animation.
// Small utility to merge class names.
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import type { ClassValue } from 'clsx'
const config = {
theme: {
extend: {
keyframes: {
'caret-blink': {
'0%,70%,100%': { opacity: '1' },
'20%,50%': { opacity: '0' },
},
},
animation: {
'caret-blink': 'caret-blink 1.2s ease-out infinite',
},
},
},
}
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
There's currently no native OTP/2FA/MFA input in HTML, which means people are either going with 1. a simple input design or 2. custom designs like this one. This library works by rendering an invisible input as a sibling of the slots, contained by a relative
ly positioned parent (the container root called OTPInput).
The root container. Define settings for the input via props. Then, pass in child elements to create the slots.
Name | Description | Type | Values | Default |
---|---|---|---|---|
maxlength |
The number of slots. | number |
- |
- |
containerClass |
The class for the root container. | string |
- |
- |
textAlign |
Where is the text located within the input. Affects click-holding or long-press behavior. | string |
left , right , center |
center |
inputmode |
Virtual keyboard appearance on mobile. | string |
numeric , text |
numeric |
pushPasswordManagerStrategy |
Detect Password Managers and shift their badges to the right side, outside the input. | string |
increase-width , none |
increase-width |
Name | Description | Props |
---|---|---|
default |
The slots to be rendered. | slots: SlotProps[], isFocused: boolean, isHovering: boolean |
Name | Description | Parameters |
---|---|---|
complete |
Emitted when the input is complete. | value: string |
Automatic form submission on OTP completion
<script setup>
import { OTPInput } from 'vue-input-otp'
import { ref } from 'vue'
const formRef = ref()
const buttonRef = ref()
function onComplete() {
// ... automatically submit the form
formRef.value.submit()
// ... or focus the button like as you wish
buttonRef.value.focus()
}
</script>
<template>
<form ref="formRef">
<OTPInput @complete="onComplete" />
<button ref="buttonRef">
Submit
</button>
</form>
</template>
Automatically focus the input when the page loads
<script setup>
import { OTPInput } from 'vue-input-otp'
</script>
<template>
<form>
<!-- Pro tip: accepts all common HTML input props... -->
<OTPInput auto-focus />
</form>
</template>
See list of caveats in the original implementation here.
MIT