Input OTP
Segmented one-time-code field with sharp mono cells, a 2px signal focus ring on the active slot, and a blinking block caret.
"use client";import { InputOTP, InputOTPGroup, InputOTPSlot,} from "@/registry/okibi/ui/input-otp";export default function InputOTPDemo() { return ( <InputOTP maxLength={6}> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} /> <InputOTPSlot index={3} /> <InputOTPSlot index={4} /> <InputOTPSlot index={5} /> </InputOTPGroup> </InputOTP> );}npx shadcn@latest add https://okibi.cndr.dev/r/input-otp.jsonInstallation
Install the theme first, then add the component:
npx shadcn@latest add https://okibi.cndr.dev/r/input-otp.jsonInstall the required dependencies:
npm install input-otp lucide-reactCopy the source from the Code tab above into components/ui/input-otp.tsx. It uses the cn helper and the okibi theme tokens — install the theme first if you haven't.
Usage
import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "@/components/ui/input-otp"
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Examples
With separator
Split the slots into groups with an InputOTPSeparator for a grouped code layout (e.g. 3-3).
"use client";import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot,} from "@/registry/okibi/ui/input-otp";export default function InputOTPSeparatorDemo() { return ( <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> );}Invalid
Set aria-invalid and every slot flips to the destructive keyline; pair it with a mono rejection line.
AUTH // code rejected — expired or incomplete.
"use client";import { InputOTP, InputOTPGroup, InputOTPSlot,} from "@/registry/okibi/ui/input-otp";export default function InputOTPInvalidDemo() { return ( <div className="space-y-2"> <InputOTP maxLength={6} defaultValue="319" aria-invalid> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} /> <InputOTPSlot index={3} /> <InputOTPSlot index={4} /> <InputOTPSlot index={5} /> </InputOTPGroup> </InputOTP> <p className="font-mono text-xs text-destructive-spark"> AUTH // code rejected — expired or incomplete. </p> </div> );}Disabled
The native disabled attribute dims and locks the whole field.
"use client";import { InputOTP, InputOTPGroup, InputOTPSlot,} from "@/registry/okibi/ui/input-otp";export default function InputOTPDisabledDemo() { return ( <InputOTP maxLength={6} defaultValue="7741" disabled> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} /> <InputOTPSlot index={3} /> <InputOTPSlot index={4} /> <InputOTPSlot index={5} /> </InputOTPGroup> </InputOTP> );}API Reference
InputOTP
| Prop | Type | Default | Description |
|---|---|---|---|
maxLength | number | – | Total number of slots / code length. |
value | string | – | Controlled value. |
onChange | (value: string) => void | – | Fires as the code is entered. |
pattern | string | – | Regex the input is validated against (e.g. digits only). |
InputOTPSlot
| Prop | Type | Default | Description |
|---|---|---|---|
index | number | – | Which slot this cell renders (0-based). |
Accessibility
- Typing is captured by one hidden input behind the slots; the visible cells are presentational, so paste and arrow keys behave like a single field.
- Setting
aria-invalidonInputOTPis propagated through context down to everyInputOTPSlot— the destructive border renders on the cells, not just the hidden input. - The active slot shows a 2px inset
ringand a blinking block caret; the caret animation is reduced-motion gated globally. InputOTPSeparatorisrole='separator'; keep its icon decorative so it isn't announced as content.- Disabling the field dims the whole group via
has-disabledand blocks pointer interaction.
Guidelines
Do
- Set
maxLengthto the exact code length and pass a digits-onlypatternwhen the code is numeric. - Drive validation by setting
aria-invalidonInputOTPso the error border reaches every slot. - Pair the field with a visible label and announce success once the code is complete.
Don't
- Don't push past ~6–8 slots; the fixed-size cells overflow narrow viewports with no wrap.
- Don't style individual slots for error by hand — let the
aria-invalidcontext drive it so every cell stays consistent.
Combobox
Autocomplete select — a Command palette on a hard-shadow popover with a mono search and a signal check on the active option. Controlled via `value`/`onValueChange`, so it drops straight into a Form field.
Toggle
Sharp two-state button that fills signal when pressed; `default` (borderless) and `outline` (keylined) variants.