Pin Input
React 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 Vue ->Installation
$ pnpm dlx shadcn@latest add https://react.uipkge.dev/r/react/pin-input.json$ npx shadcn@latest add https://react.uipkge.dev/r/react/pin-input.json$ yarn dlx shadcn@latest add https://react.uipkge.dev/r/react/pin-input.json$ bunx shadcn@latest add https://react.uipkge.dev/r/react/pin-input.json
Or with the named registry:
npx shadcn@latest add @uipkge-react/pin-input
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
maxLength Number of slots. Maps to input-otp's `maxLength`. | number | 6 | optional |
mask Render the typed characters as dots instead of plain text. | boolean | false | optional |
status | PinInputStatus | default | optional |
size | PinInputSize | md | optional |
onComplete Fires with the joined string once every slot is filled. | (value: string) => void | — | optional |
children | React.ReactNode | — | optional |
className | string | — | optional |
containerClassName | string | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
PinInputContextValue interface PinInputContextValue {
mask: boolean
status: PinInputStatus
size: PinInputSize
} Dependencies
Used by
Files (2)
-
components/ui/pin-input/pin-input.tsx 4.6 kB
'use client' import * as React from 'react' import { OTPInput, OTPInputContext } from 'input-otp' import { Minus } from 'lucide-react' import { cn } from '@/lib/utils' type PinInputStatus = 'error' | 'warning' | 'success' | 'default' type PinInputSize = 'sm' | 'md' | 'lg' interface PinInputContextValue { mask: boolean status: PinInputStatus size: PinInputSize } const PinInputUiContext = React.createContext<PinInputContextValue>({ mask: false, status: 'default', size: 'md', }) export interface PinInputProps extends Omit<React.ComponentPropsWithoutRef<typeof OTPInput>, 'render' | 'children' | 'maxLength' | 'size'> { /** Number of slots. Maps to input-otp's `maxLength`. */ maxLength?: number /** Render the typed characters as dots instead of plain text. */ mask?: boolean status?: PinInputStatus size?: PinInputSize /** Fires with the joined string once every slot is filled. */ onComplete?: (value: string) => void children?: React.ReactNode className?: string containerClassName?: string } const PinInput = React.forwardRef<React.ElementRef<typeof OTPInput>, PinInputProps>( ( { className, containerClassName, maxLength = 6, mask = false, status = 'default', size = 'md', onComplete, children, ...props }, ref, ) => { return ( <PinInputUiContext.Provider value={{ mask, status, size }}> <OTPInput ref={ref} data-uipkge="" data-slot="pin-input" maxLength={maxLength} onComplete={onComplete} containerClassName={cn( 'flex items-center gap-2 has-disabled:opacity-50', containerClassName, )} className={cn('disabled:cursor-not-allowed', className)} {...props} > {children} </OTPInput> </PinInputUiContext.Provider> ) }, ) PinInput.displayName = 'PinInput' const PinInputGroup = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>( ({ className, ...props }, ref) => ( <div ref={ref} data-uipkge="" data-slot="pin-input-group" className={cn('flex items-center', className)} {...props} /> ), ) PinInputGroup.displayName = 'PinInputGroup' const sizeClassMap: Record<PinInputSize, string> = { sm: 'h-8 w-8 text-sm', lg: 'h-12 w-12 text-xl', md: 'h-10 w-10 text-base', } const statusClassMap: Record<PinInputStatus, string> = { error: 'border-destructive focus-within:border-destructive focus-within:ring-destructive/40 text-destructive', warning: 'border-warning focus-within:border-warning focus-within:ring-warning/40 text-warning', success: 'border-success focus-within:border-success focus-within:ring-success/40 text-success', default: '', } export interface PinInputSlotProps extends React.ComponentPropsWithoutRef<'div'> { index: number /** Override the inherited mask flag for this slot. */ mask?: boolean } const PinInputSlot = React.forwardRef<HTMLDivElement, PinInputSlotProps>( ({ index, mask: maskProp, className, ...props }, ref) => { const ui = React.useContext(PinInputUiContext) const inputContext = React.useContext(OTPInputContext) const slot = inputContext.slots[index] const char = slot?.char ?? null const isActive = slot?.isActive ?? false const effectiveMask = maskProp ?? ui.mask return ( <div ref={ref} data-uipkge="" data-slot="pin-input-slot" data-active={isActive ? '' : undefined} className={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-within:border-ring focus-within:ring-ring/40 focus-within:relative focus-within:z-10 focus-within:ring-2', 'disabled:cursor-not-allowed disabled:opacity-50', isActive && 'border-ring ring-ring/40 z-10 ring-2', sizeClassMap[ui.size], statusClassMap[ui.status], className, )} {...props} > {char != null && (effectiveMask ? <span className="bg-foreground size-2 rounded-full" /> : char)} </div> ) }, ) PinInputSlot.displayName = 'PinInputSlot' const PinInputSeparator = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>( ({ children, ...props }, ref) => ( <div ref={ref} data-uipkge="" data-slot="pin-input-separator" role="separator" {...props}> {children ?? <Minus />} </div> ), ) PinInputSeparator.displayName = 'PinInputSeparator' export { PinInput, PinInputGroup, PinInputSlot, PinInputSeparator } -
components/ui/pin-input/index.ts 0.1 kB
export { PinInput, PinInputGroup, PinInputSlot, PinInputSeparator, type PinInputProps, type PinInputSlotProps, } from './pin-input'
Raw manifest: https://react.uipkge.dev/r/react/pin-input.json