Pin Input
Vue formOne-time-code input — N separate boxes that auto-advance and accept paste. Use for SMS verification, 2FA, and short numeric codes. Length, masking, and per-slot status all configurable.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/pin-input.json$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/pin-input.json$ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/pin-input.json$ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/pin-input.json
Or with the named registry:
npx shadcn-vue@latest add @uipkge/pin-input
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
mask | boolean | false | optional |
autoSubmit | boolean | false | optional |
status | PinInputStatus | 'default' | optional |
size | PinInputSize | 'md' | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
PinInputContext interface PinInputContext {
mask: Ref<boolean>
status: Ref<PinInputStatus>
size: Ref<PinInputSize>
} Dependencies
Used by
Files (5)
-
app/components/ui/pin-input/PinInput.vue 1.7 kB
<script setup lang="ts" generic="Type extends 'text' | 'number' = 'text'"> import { computed, provide, toRef } from 'vue' import type { PinInputRootEmits, PinInputRootProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { PinInputRoot, useForwardPropsEmits } from 'reka-ui' import { cn } from '@/lib/utils' type PinInputStatus = 'error' | 'warning' | 'success' | 'default' type PinInputSize = 'sm' | 'md' | 'lg' const props = withDefaults( defineProps< PinInputRootProps<Type> & { class?: HTMLAttributes['class'] mask?: boolean autoSubmit?: boolean status?: PinInputStatus size?: PinInputSize } >(), { otp: true, mask: false, autoSubmit: false, status: 'default', size: 'md', }, ) const emits = defineEmits< Omit<PinInputRootEmits<Type>, 'complete'> & { complete: [value: string] } >() const delegatedProps = reactiveOmit(props, 'class', 'mask', 'autoSubmit', 'status', 'size') const forwarded = useForwardPropsEmits(delegatedProps, emits) provide('pinInputContext', { mask: toRef(props, 'mask'), status: toRef(props, 'status'), size: toRef(props, 'size'), }) function handleComplete(value: string[]) { const joined = value.join('') emits('complete', joined) if (props.autoSubmit) { const event = new CustomEvent('pin-submit', { detail: joined, bubbles: true }) document.dispatchEvent(event) } } </script> <template> <PinInputRoot :otp="props.otp" data-uipkge data-slot="pin-input" v-bind="forwarded" :class="cn('flex items-center gap-2 disabled:cursor-not-allowed has-disabled:opacity-50', props.class)" @complete="handleComplete" > <slot /> </PinInputRoot> </template> -
app/components/ui/pin-input/PinInputGroup.vue 0.6 kB
<script setup lang="ts"> import type { PrimitiveProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { Primitive, useForwardProps } from 'reka-ui' import { cn } from '@/lib/utils' const props = defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>() const delegatedProps = reactiveOmit(props, 'class') const forwardedProps = useForwardProps(delegatedProps) </script> <template> <Primitive data-uipkge data-slot="pin-input-group" v-bind="forwardedProps" :class="cn('flex items-center', props.class)" > <slot /> </Primitive> </template> -
app/components/ui/pin-input/PinInputSeparator.vue 0.4 kB
<script setup lang="ts"> import type { PrimitiveProps } from 'reka-ui' import { Minus } from 'lucide-vue-next' import { Primitive, useForwardProps } from 'reka-ui' const props = defineProps<PrimitiveProps>() const forwardedProps = useForwardProps(props) </script> <template> <Primitive data-uipkge data-slot="pin-input-separator" v-bind="forwardedProps"> <slot> <Minus /> </slot> </Primitive> </template> -
app/components/ui/pin-input/PinInputSlot.vue 2.3 kB
<script setup lang="ts"> import { computed, inject } from 'vue' import type { PinInputInputProps } from 'reka-ui' import type { HTMLAttributes, Ref } from 'vue' import { reactiveOmit } from '@vueuse/core' import { PinInputInput, useForwardProps } from 'reka-ui' import { cn } from '@/lib/utils' type PinInputStatus = 'error' | 'warning' | 'success' | 'default' type PinInputSize = 'sm' | 'md' | 'lg' interface PinInputContext { mask: Ref<boolean> status: Ref<PinInputStatus> size: Ref<PinInputSize> } const props = defineProps< PinInputInputProps & { class?: HTMLAttributes['class'] /** Override the inherited mask flag for this slot. */ mask?: boolean } >() const ctx = inject<PinInputContext | null>('pinInputContext', null) const forwarded = useForwardProps(reactiveOmit(props, 'class', 'mask')) const effectiveMask = computed(() => props.mask ?? ctx?.mask.value ?? false) const status = computed(() => ctx?.status.value ?? 'default') const size = computed(() => ctx?.size.value ?? 'md') const inputType = computed(() => (effectiveMask.value ? 'password' : 'text')) const sizeClasses = computed(() => { switch (size.value) { case 'sm': return 'h-8 w-8 text-sm' case 'lg': return 'h-12 w-12 text-xl' default: return 'h-10 w-10 text-base' } }) const statusClasses = computed(() => { switch (status.value) { case 'error': return 'border-destructive focus:border-destructive focus:ring-destructive/40 text-destructive' case 'warning': return 'border-warning focus:border-warning focus:ring-warning/40 text-warning' case 'success': return 'border-success focus:border-success focus:ring-success/40 text-success' default: return '' } }) </script> <template> <PinInputInput :type="inputType" data-uipkge data-slot="pin-input-slot" v-bind="forwarded" :class=" cn( 'border-input bg-background text-foreground relative -ml-px flex items-center justify-center border text-center shadow-xs transition-[border-color,box-shadow] outline-none first:ml-0 first:rounded-l-md last:rounded-r-md', 'focus:border-ring focus:ring-ring/40 focus:relative focus:z-10 focus:ring-2', 'disabled:cursor-not-allowed disabled:opacity-50', sizeClasses, statusClasses, props.class, ) " /> </template> -
app/components/ui/pin-input/index.ts 0.2 kB
export { default as PinInput } from './PinInput.vue' export { default as PinInputGroup } from './PinInputGroup.vue' export { default as PinInputSeparator } from './PinInputSeparator.vue' export { default as PinInputSlot } from './PinInputSlot.vue'
Raw manifest: https://uipkge.dev/r/vue/pin-input.json