Skip to main content
Gremorie

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

Installation

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>
  );
}

Angular edition planned for Phase 5h. Star the repo to track progress.

API

<InputOTP>

PropTypeDefaultDescription
maxLengthnumber-Total number of slots. Required.
valuestring-Controlled value.
onChange(value: string) => void-Fires on every keystroke and paste.
patternRegExp | string-Restrict allowed characters (e.g. ^[0-9]+$ for digits only).
containerClassNamestring-Classes applied to the wrapper row, not the underlying input.
classNamestring-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>

PropTypeDefaultDescription
indexnumber-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

  1. <InputOTP> is the controlled root - owns value, maxLength, and the underlying hidden input.
  2. <InputOTPGroup> is a visual cluster. You can render multiple per InputOTP and separate them with <InputOTPSeparator />.
  3. <InputOTPSlot index={n} /> is one character cell. Slots read their state from the shared OTPInputContext based 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").

<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.

<InputOTP maxLength={6} pattern="^[0-9]+$">
  <InputOTPGroup>
    {Array.from({ length: 6 }).map((_, i) => (
      <InputOTPSlot key={i} index={i} />
    ))}
  </InputOTPGroup>
</InputOTP>

Accessibility

  • Single accessible input: behind the scenes, input-otp renders 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; Backspace clears 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.
  • Input - the canonical single-line text field
  • Form - wire OTP into react-hook-form for verification flows
  • Button - the typical "Verify" trigger underneath the OTP

On this page