Vue One-Time Passcode Input
vue-input-otp is an accessible and unstyled Vue 3 component designed for capturing one-time passcodes (OTPs). It provides a flexible slot-based API, allowing developers complete control over the visual presentation of each digit input, rather than enforcing a specific UI. The library works by rendering an invisible native input that handles the underlying logic, while exposing slots for custom rendering. The current stable version is 0.3.2, with minor and patch releases occurring as needed. Its key differentiator is its 'bring-your-own-style' approach, offering maximum customization and robust accessibility features, including password manager support and precise control over virtual keyboard behavior on mobile devices. It aims to solve the lack of a native HTML OTP input by providing a robust, extensible solution for Vue applications.
Common errors
-
[Vue warn]: Component <OTPInput> is missing template or render function.
cause The OTPInput component requires content in its default slot to render the individual OTP digits. If you don't provide a slot template, Vue doesn't know what to render inside the component.fixEnsure you are passing a default slot to `OTPInput` that iterates over `slots` provided by the component's scope, as shown in the usage examples. E.g., `<OTPInput v-slot="{ slots }">...</OTPInput>`. -
Property 'char' does not exist on type 'SlotProps'.
cause This error occurs in TypeScript when you're trying to access a property like `char` or `isActive` on a slot prop without correctly typing your slot component or props.fixWhen defining your custom slot component, import `SlotProps` from `vue-input-otp` and use it to type your component's props. For example: `import type { SlotProps } from 'vue-input-otp'; defineProps<SlotProps>();`.
Warnings
- breaking The `input` event on the underlying native input now emits the raw value directly, rather than the event object. If you were listening for the event object, your handler will need to be updated.
- breaking The default pattern for input validation was removed. This change provides more flexibility but requires users to implement their own validation if specific input patterns are needed.
- gotcha vue-input-otp is intentionally unstyled. Developers are responsible for providing all visual styling for the OTP slots. Failure to do so will result in an invisible or poorly rendered input.
- gotcha To ensure proper functionality with password managers, especially those that inject badges, consider using the `pushPasswordManagerStrategy` prop. Incorrect handling can lead to visual glitches or obscured input.
Install
-
npm install vue-input-otp -
yarn add vue-input-otp -
pnpm add vue-input-otp
Imports
- OTPInput
import OTPInput from 'vue-input-otp'
import { OTPInput } from 'vue-input-otp' - SlotProps
import { SlotProps } from 'vue-input-otp'import type { SlotProps } from 'vue-input-otp' - useVueOTPContext
import { useVueOTPContext } from 'vue-input-otp'
Quickstart
<script setup lang="ts">
import { ref } from 'vue';
import { OTPInput } from 'vue-input-otp';
const input = ref('123456');
// Example Slot component (simplified for brevity, assume 'cn' utility and 'isActive' prop are defined elsewhere)
// In a real app, you'd likely define this in a separate file, e.g., 'Slot.vue'
const Slot = {
props: ['char', 'isActive'],
template: `
<div
:class="[
'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>
<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>
`
};
</script>
<template>
<form class="flex flex-col gap-4 p-4">
<label for="otp-input" class="text-lg font-medium">Enter OTP:</label>
<OTPInput
id="otp-input"
v-slot="{ slots }"
v-model="input"
: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>
<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>
<p class="mt-2 text-sm text-gray-600">Current value: {{ input }}</p>
</form>
</template>