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>
);
}npx gremorie@latest add ng-input-otpPrerequisite
Angular components emit Tailwind utility classes and ship no compiled CSS. Without the setup below in src/styles.css the component mounts but renders completely unstyled. Requires Tailwind CSS v4.
@import 'tailwindcss';@import '@gremorie/tokens/theme.css';/* Required. Tailwind v4 skips node_modules by default, so without this line the components' utility classes never reach your compiled CSS and every Gremorie component renders unstyled. */@source '../node_modules/@gremorie';The @source line is required: Tailwind v4 skips node_modules by default. Adjust the relative path if your stylesheet is not at src/styles.css. Full setup in Installation.
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.
Related
Input Group
Composable input layout that wraps Input or Textarea with inline and block addons (icons, buttons, kbd hints) while preserving group-wide focus and error states.
Textarea
Multi-line text field that auto-grows via `field-sizing: content`, with the same token-driven focus and error states as Input.