Components
- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input
- Input OTP
- Input Group
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Native Select
- Navigation Menu
- Pagination
- Popover
- Progress
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Toggle Group
- Tooltip
- New Component
Elements
Charts
Icons
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@/components/ui/input-otp";
export const InputOTPDemo = () => (
<div className="flex flex-col items-center gap-6">
{/* Basic 6-digit */}
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
{/* Two groups with separator */}
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</div>
);With separator
Use InputOTPSeparator between groups to split slots visually — common for 6-digit codes split into two groups of three.
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Controlled
Manage the value externally with value and onChange.
const [value, setValue] = React.useState("");
<InputOTP maxLength={6} value={value} onChange={setValue}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
{value.length === 6 && (
<p className="text-sm text-muted-foreground">Code entered: {value}</p>
)}Pattern validation
Restrict input to digits only with pattern={REGEXP_ONLY_DIGITS}, or digits and chars with REGEXP_ONLY_DIGITS_AND_CHARS.
import { REGEXP_ONLY_DIGITS } from "input-otp";
<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Submit on complete
Use onComplete to automatically submit when all slots are filled.
<InputOTP
maxLength={6}
onComplete={(code) => verifyCode(code)}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Disabled state
<InputOTP maxLength={6} disabled>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Props
InputOTP
Extends all props fromOTPInput (the input-otp package).
| Prop | Type | Default | Description |
|---|---|---|---|
maxLength | number | — | Total number of OTP characters (required) |
value | string | — | Controlled value |
onChange | (value: string) => void | — | Called on every keystroke |
onComplete | (value: string) => void | — | Called when all slots are filled |
pattern | string | — | Regex to restrict allowed characters |
disabled | boolean | false | Disables the entire input |
containerClassName | string | — | Applied to the outer flex container |
InputOTPSlot
| Prop | Type | Default | Description |
|---|---|---|---|
index | number | — | Position in the OTP sequence (required) |
Usage
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Specs
# Mint — Input OTP Specs
## Component
- name: input-otp
- file: registry/mint/input-otp.tsx
- installs to: components/ui/input-otp.tsx
- dependencies: input-otp, lucide-react
- registryDependencies: none
## What is it
A one-time password input built on the `input-otp` library. It renders individual
character slots backed by a single hidden `<input>` — keyboard navigation, clipboard
paste, and screen-reader announcements are handled by the library. Use it for
verification codes, PINs, and any fixed-length character entry. Do not use it for
variable-length or freeform text input.
## Visual identity
Compact square slots (36 × 36 px) share a continuous border to form a pill-like
group. Mint's generous radius (1.25rem) is applied only to the outer corners of each
group. A 3 px ring in --ring highlights the active slot with a soft green glow
against the mint-tinted border.
## Design tokens
--input: oklch(0.88 0.04 155 / 60%) / oklch(0.32 0.05 155 / 40%) — slot border (border-input)
--ring: oklch(0.5 0.14 155) / oklch(0.62 0.15 155) — active-slot focus ring (ring-ring/50)
--foreground: oklch(0.145 0 0) / oklch(0.985 0 0) — blinking caret bar (bg-foreground)
--destructive: oklch(0.55 0.25 15) / oklch(0.704 0.191 22.216) — invalid border (aria-invalid:border-destructive)
--radius: 1.25rem — group corner rounding
## Sub-components
- InputOTP → wraps OTPInput; accepts containerClassName for the outer flex container
- InputOTPGroup → flex row that groups consecutive slots into a single visual block
- InputOTPSlot → single character cell; reads char/isActive/hasFakeCaret from OTPInputContext; index prop required
- InputOTPSeparator → role="separator" div rendering MinusIcon; purely decorative
## External dependencies
- input-otp: provides OTPInput, OTPInputContext, and pattern constants (REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS)
## States
- isActive: border-ring ring-ring/50 z-10 ring-[3px] — 3 px ring highlights the focused slot
- hasFakeCaret: animate-caret-blink blinking bar centred in the slot when no char is entered yet
- aria-invalid: border-destructive — red border on invalid slots
- disabled: has-disabled:opacity-50 on the container; disabled:cursor-not-allowed on the hidden input
## Conventions
- Always index slots globally from 0 across all groups, not per-group
- Use a single InputOTPGroup for a compact block; add InputOTPSeparator between groups to split codes visually (e.g. 3-3 or 4-4)
- Restrict input via the pattern prop: import REGEXP_ONLY_DIGITS from "input-otp" for numeric codes
- Use onComplete to auto-submit when all slots are filled — pair with a loading state to prevent double-submission
- The hidden input manages all focus and paste behaviour; do not add click handlers to individual slots
- containerClassName on InputOTP is the correct prop for layout overrides; className targets the hidden input onlyInstallation
$ npx shadcn add \https://gin-design.vercel.app/input-otp