Input OTP
Segmented one-time-password input built on the `input-otp` library, with paste-to-fill, autocomplete, and inter-slot focus management.
Overview
InputOTP renders a row of single-character slots that behave as a single underlying <input>. Built on top of input-otp, it handles inter-slot caret movement, paste-to-fill, and the browser's autocomplete="one-time-code" so SMS / email codes auto-populate on supported platforms.
Use it for 2FA, email verification, SMS confirmation - any ephemeral code the user types or pastes. Do not use it for passwords or persistent PINs; those need a regular Input with type="password".
Preview
'use client';import { InputOTP, InputOTPGroup, InputOTPSlot } from '@gremorie/rx-forms';export function InputOTPPreview() { 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> );}Anatomy
InputOTP hidden OTPInput owning value + maxLength
├─ InputOTPGroup visual cluster of contiguous slots
│ └─ InputOTPSlot one character cell (reads its char by index)
├─ InputOTPSeparator a MinusIcon divider between groups
└─ InputOTPGroup another cluster (split codes 3 + 3)
└─ InputOTPSlot one character cellInstallation
bash npx gremorie@latest add rx-input-otp bash pnpm dlx gremorie@latest add rx-input-otp bash yarn dlx gremorie@latest add rx-input-otp bash bunx --bun gremorie@latest add rx-input-otp Usage
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@gremorie/rx-forms";
export function Example() {
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>
);
}The Angular edition of this component ships from source today (see the workbench for the side-by-side); its registry entry is coming next.
API
<InputOTP>
| Prop | Type | Default | Description |
|---|---|---|---|
maxLength | number | - | Total number of slots. Required. |
value | string | - | Controlled value. |
onChange | (value: string) => void | - | Fires on every keystroke and paste. |
pattern | RegExp | string | - | Restrict allowed characters (e.g. ^[0-9]+$ for digits only). |
containerClassName | string | - | Classes applied to the wrapper row, not the underlying input. |
className | string | - | Classes applied to the underlying input (kept visually hidden). |
Extends all React.ComponentProps<typeof OTPInput> from input-otp. Renders with autocomplete="one-time-code" by default so the browser offers to autofill SMS codes on supported devices.
<InputOTPGroup>
Visual cluster of InputOTPSlots. Adds flex items-center and lets you split codes into chunks (e.g. 3 + 3) by rendering multiple groups separated by <InputOTPSeparator />.
Extends all React.ComponentProps<"div">.
<InputOTPSlot>
| Prop | Type | Default | Description |
|---|---|---|---|
index | number | - | Position in the underlying value (0-based). Required. |
Reads the active char, focus state, and fake caret position from OTPInputContext and renders the corresponding visual.
<InputOTPSeparator>
Decorative <div role="separator"> rendering a minus icon. Use between groups.
Composition
<InputOTP>is the controlled root - ownsvalue,maxLength, and the underlying hidden input.<InputOTPGroup>is a visual cluster. You can render multiple perInputOTPand separate them with<InputOTPSeparator />.<InputOTPSlot index={n} />is one character cell. Slots read their state from the sharedOTPInputContextbased on their index.
Variations
Six-digit code (single group)
The most common pattern for 2FA. maxLength={6} and six slots inside one group.
<InputOTP maxLength={6}>
<InputOTPGroup>
{Array.from({ length: 6 }).map((_, i) => (
<InputOTPSlot key={i} index={i} />
))}
</InputOTPGroup>
</InputOTP>Four-digit PIN
Short PIN code for confirmations. Reduce maxLength and the slot count together.
<InputOTP maxLength={4}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>Grouped 3 + 3 with separator
Split a six-digit code with a separator for legibility (mirrors how codes are usually presented in emails: "123 456").
'use client';import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot,} from '@gremorie/rx-forms';export function InputOTPSeparatorPreview() { 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> );}Numeric-only pattern
Restrict input to digits via the pattern prop. Prevents users from typing letters at all.
'use client';import { InputOTP, InputOTPGroup, InputOTPSlot } from '@gremorie/rx-forms';// Restrict every slot to digits only. `pattern` is the regex string the// underlying input-otp library validates each keystroke against.const DIGITS_ONLY = '^\\d*$';export function InputOTPPatternPreview() { return ( <InputOTP maxLength={6} pattern={DIGITS_ONLY}> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} /> <InputOTPSlot index={3} /> <InputOTPSlot index={4} /> <InputOTPSlot index={5} /> </InputOTPGroup> </InputOTP> );}Disabled
disabled on the root dims every slot and removes the field from the tab order. The current value stays readable.
'use client';import { InputOTP, InputOTPGroup, InputOTPSlot } from '@gremorie/rx-forms';export function InputOTPDisabledPreview() { return ( <InputOTP maxLength={6} disabled defaultValue="123456"> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} /> <InputOTPSlot index={3} /> <InputOTPSlot index={4} /> <InputOTPSlot index={5} /> </InputOTPGroup> </InputOTP> );}Accessibility
- Single accessible input: behind the scenes,
input-otprenders one<input>element that owns the value, type, name, and label association. Screen readers see one field, not six. - Autocomplete:
autocomplete="one-time-code"lets iOS, Android, and modern browsers offer SMS auto-fill. - Keyboard navigation: typing moves the caret forward;
Backspaceclears and moves back; arrow keys move between slots; selection-to-paste spreads characters across slots. - Paste-to-fill: pasting a full code into any slot distributes characters across all slots in order.
- Active slot: the focused slot renders a blinking caret (
animate-caret-blink) and a focus ring, so users always see where the next keystroke lands. - Invalid state:
aria-invalid="true"on the root switches every slot's border to the destructive token.