A polished one-time-password input with animated cells, a pulsing caret, spring-loaded digits, success glow and error shake.
Enter the 6-digit code (try 123456).
"use client";
import { useState } from "react";
import { OtpInput } from "@/components/matos-ui/otp-input";
const DEMO_CODE = "123456";
export function OtpInputDemo() {
const [value, setValue] = useState("");
const [error, setError] = useState<string | undefined>(undefined);
const [verified, setVerified] = useState(false);
function handleChange(next: string) {
setValue(next);
if (error) {
setError(undefined);
}
if (verified) {
setVerified(false);
}
}
function handleComplete(next: string) {
if (next === DEMO_CODE) {
setVerified(true);
setError(undefined);
} else {
setError("Invalid code. Try 123456.");
setVerified(false);
}
}
return (
<div className="flex w-full max-w-md flex-col gap-8">
<OtpInput
label="Verification code"
description={
verified
? "Code verified — welcome back."
: "Enter the 6-digit code (try 123456)."
}
error={error}
value={value}
onChange={handleChange}
onComplete={handleComplete}
groupSize={3}
/>
<div className="flex flex-col gap-3 border-border/60 border-t pt-6">
<span className="text-muted-foreground text-xs">Sizes</span>
<div className="flex flex-wrap items-end gap-6">
<OtpInput size="sm" length={4} defaultValue="12" />
<OtpInput size="md" length={4} defaultValue="34" />
<OtpInput size="lg" length={4} defaultValue="56" />
</div>
</div>
<div className="flex flex-col gap-3 border-border/60 border-t pt-6">
<span className="text-muted-foreground text-xs">
Subtle, masked & disabled
</span>
<div className="flex flex-wrap items-center gap-6">
<OtpInput variant="subtle" length={5} defaultValue="42" />
<OtpInput mask length={5} defaultValue="903" />
<OtpInput disabled length={4} defaultValue="7788" />
</div>
</div>
</div>
);
}
pnpm dlx shadcn@latest add https://matos-ui.com/r/otp-input.jsonimport { OtpInput } from '@/components/matos-ui/otp-input'<OtpInput
label="Verification code"
length={6}
onComplete={(code) => verify(code)}
/>Use the OTP Input for short, fixed-length codes: two-factor authentication, email
or SMS verification, and PIN entry. It behaves like a single field — typing,
pasting, autofill (autocomplete="one-time-code") and keyboard navigation all
work out of the box — while rendering one animated cell per character.
Drive the value yourself and react to onComplete to verify the code. Setting
error turns the cells destructive and triggers a subtle shake.
const [value, setValue] = useState('')
const [error, setError] = useState<string>()
<OtpInput
label="Verification code"
value={value}
onChange={setValue}
error={error}
onComplete={(code) => {
if (code !== '123456') setError('Invalid code.')
}}
/>Use groupSize to split the cells with separators (for example XXX-XXX).
<OtpInput length={6} groupSize={3} />Hide the characters behind dots for sensitive PINs.
<OtpInput type="numeric" mask length={4} />Accept letters and numbers for invite or backup codes.
<OtpInput type="alphanumeric" length={8} />chart-2 glow sweeps over the group when every cell is filled.| Prop | Type | Default | Description |
|---|---|---|---|
length | number | 6 | Number of cells / characters. |
value | string | — | Controlled value. |
defaultValue | string | "" | Initial value when uncontrolled. |
onChange | (value: string) => void | — | Called whenever the value changes. |
onComplete | (value: string) => void | — | Called once when all cells are filled. |
type | "numeric" | "alphanumeric" | "text" | "numeric" | Allowed characters and input mode. |
mask | boolean | false | Render dots instead of the typed characters. |
groupSize | number | — | Insert a separator after every N cells. |
size | "sm" | "md" | "lg" | "md" | Cell size. |
variant | "default" | "subtle" | "default" | Visual style of the cells. |
label | ReactNode | — | Field label. |
description | ReactNode | — | Helper text below the field. |
error | ReactNode | — | Error message; marks the field invalid and shakes it. |
invalid | boolean | false | Force the invalid state without a message. |
disabled | boolean | false | Disable the input. |
Also exported: otpInputVariants and the OtpInputProps / OtpInputType types.
type="numeric" for SMS/email codes so mobile keyboards show digits.label (or rely on the built-in aria-label) for screen readers.error / description text, not color alone.Install Matos UI
Choose a package manager and copy one command for every component.