Input OTP
OTP input backed by Base UI OTPFieldPreview with grouped slots, separator, and numeric/alphanumeric validation modes.
Basic
6 slots — default numeric OTP
Value: —
4 slots — PIN
separator — 3+3 split
separator={4} — 4+4 split on 8 slots
Variants
mask — password mode
validationType: alphanumeric
State
Disabled
OTPWrapper (field-wrapper)
With label
Error state
Invalid code. Please try again.
Installation
npx shadcn@latest add @glrk-ui/input-otpIf you haven't set up the prerequisites yet, check out Prerequest section.
Copy and paste the following code into your project.
'use client'
import * as React from 'react'
import { OTPFieldPreview as OTPFieldPrimitive } from '@base-ui/react/otp-field'
import { MinusIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
function InputOTP({
className,
...props
}: React.ComponentProps<typeof OTPFieldPrimitive.Root>) {
return (
<OTPFieldPrimitive.Root
data-slot="input-otp"
className={cn('flex items-center data-[disabled]:opacity-50', className)}
{...props}
/>
)
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="input-otp-group"
className={cn('flex items-center', className)}
{...props}
/>
)
}
function InputOTPSlot({
className,
...props
}: React.ComponentProps<typeof OTPFieldPrimitive.Input>) {
return (
<OTPFieldPrimitive.Input
data-slot="input-otp-slot"
className={cn(
'size-9 border-y border-r border-input bg-transparent text-center text-sm transition-all outline-none',
'first:rounded-l-md first:border-l last:rounded-r-md',
'focus:border-ring focus:ring-3 focus:ring-ring/50',
'data-[invalid]:border-destructive data-[focused]:data-[invalid]:ring-destructive/20',
'dark:bg-input/30 dark:data-[focused]:data-[invalid]:ring-destructive/40',
'disabled:cursor-not-allowed',
className,
)}
{...props}
/>
)
}
function InputOTPSeparator({
children,
className,
...props
}: React.ComponentProps<typeof OTPFieldPrimitive.Separator>) {
return (
<OTPFieldPrimitive.Separator
data-slot="input-otp-separator"
className={cn('flex items-center [&_svg:not([class*="size-"])]:size-4', className)}
{...props}
>
{children ?? <MinusIcon />}
</OTPFieldPrimitive.Separator>
)
}
type InputOTPWrapperProps = Omit<React.ComponentProps<typeof OTPFieldPrimitive.Root>, 'length'> & {
length?: number
separator?: boolean | number
slotClassName?: string
}
function InputOTPWrapper({
length = 6,
separator = false,
slotClassName,
className,
...props
}: InputOTPWrapperProps) {
const splitAt =
separator === false ? null : separator === true ? Math.floor(length / 2) : separator
if (splitAt === null) {
return (
<InputOTP length={length} className={className} {...props}>
<InputOTPGroup>
{Array.from({ length }, (_, i) => (
<InputOTPSlot key={i} className={slotClassName} />
))}
</InputOTPGroup>
</InputOTP>
)
}
return (
<InputOTP length={length} className={className} {...props}>
<InputOTPGroup>
{Array.from({ length: splitAt }, (_, i) => (
<InputOTPSlot key={i} className={slotClassName} />
))}
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
{Array.from({ length: length - splitAt }, (_, i) => (
<InputOTPSlot key={i} className={slotClassName} />
))}
</InputOTPGroup>
</InputOTP>
)
}
export {
InputOTP,
InputOTPGroup,
InputOTPSlot,
InputOTPSeparator,
InputOTPWrapper,
type InputOTPWrapperProps
}
Usage
Standalone (InputOTPWrapper)
import { InputOTPWrapper } from "@/components/ui/input-otp"
const [value, setValue] = useState('')
<InputOTPWrapper
length={6}
value={value}
onValueChange={(v) => setValue(v)}
/>With separator
// Split in half automatically (3+3 for length=6)
<InputOTPWrapper length={6} separator />
// Split after specific slot (4+4)
<InputOTPWrapper length={8} separator={4} />Mask (password mode)
<InputOTPWrapper length={6} mask />Alphanumeric validation
<InputOTPWrapper length={6} validationType="alphanumeric" />With field label and error (OTPWrapper)
OTPWrapper wraps InputOTPWrapper inside a Field with label and error display. Import from field-wrapper:
import { OTPWrapper } from "@/components/ui/field-wrapper"
<OTPWrapper
name="otp"
label="Verification code"
length={6}
separator
value={value}
onValueChange={(v) => setValue(v)}
/>With validation error
<OTPWrapper
name="otp"
label="Verification code"
length={6}
invalid
error={{ message: "Invalid code. Please try again." }}
/>React Hook Form
import { OTPWrapper } from "@/components/ui/field-wrapper-rhf"
<OTPWrapper name="otp" label="Verification code" control={form.control} length={6} />TanStack Form
<form.AppField
name="otp"
validators={{
onChange: ({ value }) =>
value.length < 6 ? "Enter all 6 digits" : undefined,
}}
>
{field => <field.OTPField label="Verification code" length={6} separator />}
</form.AppField>Using primitives
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
InputOTPSeparator,
} from "@/components/ui/input-otp"
<InputOTP length={6} value={value} onValueChange={setValue}>
<InputOTPGroup>
<InputOTPSlot />
<InputOTPSlot />
<InputOTPSlot />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot />
<InputOTPSlot />
<InputOTPSlot />
</InputOTPGroup>
</InputOTP>Reference
InputOTPWrapper
Prop
Type