Skip to Content
ComponentsInputOTP

InputOTP

A one-time code, one character per box.

Live Examples

Live Example
☀️
Copy
View Code
import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator, Label } from '@jarllyng/nostromo' export default function InputOTPExample() { const [code, setCode] = React.useState('') return ( <div className="space-y-2"> <Label htmlFor="otp">Verification code</Label> <InputOTP id="otp" maxLength={6} value={code} onChange={setCode}>...

Four boxes, no separator

Live Example
☀️
Copy
View Code
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@jarllyng/nostromo' export default function InputOTPFour() { return ( <InputOTP maxLength={4} onComplete={(value) => window.alert('Code: ' + value)}> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} />...

onComplete fires when the last box is filled, which is usually where you submit.

Import

import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator, } from "@jarllyng/nostromo";

Parts

PartWhat it is
InputOTPThe real input, invisible, plus the wrapper the boxes sit in
InputOTPGroupA run of boxes that share borders
InputOTPSlotOne box. Takes index, from 0
InputOTPSeparatorThe gap between groups. Decorative

Props

PropTypeDefaultDescription
maxLengthnumber-Required. How many characters
valuestring-Controlled value
onChange(value: string) => void-Gets the whole code, not an event
onComplete(value: string) => void-Fires when the last box is filled
patternstring-Regex source. Characters that fail are dropped
disabledbooleanfalseNot interactive
containerClassNamestring-Class for the row of boxes

className goes on the hidden input, not on the boxes. That is what containerClassName is for.

One input, not six

There is exactly one <input> under here, stretched across the whole row and made invisible. The boxes are <div>s that read their character from context.

That is the point. The browser sees a single ordinary text field, so paste, autofill, autocomplete="one-time-code", backspace, mobile keyboards and password managers all work without special handling. Six real inputs look identical on screen and get every one of those wrong.

Two consequences worth knowing:

  • The caret is drawn by CSS, because the real caret is not where the boxes are. It respects prefers-reduced-motion and stops blinking.
  • The boxes are aria-hidden. Everything a screen reader needs is on the input, and announcing the boxes as well would read the code out twice.

Slot indices are positional

InputOTPSlot takes an index, and the indices must run 0 to maxLength - 1 in the order they appear. An index outside that range throws rather than rendering a box that stays empty forever.

Nothing enforces the ordering, because the grouping is a visual choice: two groups of three, one group of four, a separator or not.

Only digits

pattern takes a regex source string. Characters that do not match are dropped as they are typed.

<InputOTP maxLength={6} pattern="^[0-9]+$"> ... </InputOTP>
Last updated on