OtpInput

A one-time code or PIN — cells drawn over one hidden field, with autofill and paste.

Open Source

Installation

OtpInput ships with the library — install it once, then import:

import { OtpInput } from "@delacour/react-native-ui/otp-input";

Or copy the source into your project, to own and edit it:

bunx delacour@latest add otp-input

Usage

app/index.tsx
import { OtpInput } from "@/components/ui/otp-input";
import { View } from "react-native";

export default function Example() {
  return (
    <View className="flex-1 items-center justify-center p-4">
      <OtpInput groupSize={3} onComplete={(code) => console.log(code)} />
    </View>
  );
}

Autofill

iOS offers the code from Messages on the QuickType bar and Android's autofill service offers it from an SMS, with nothing to configure in the app.

Neither kind of autofill can be exercised on a simulator. A paste can — the iOS Simulator's pasteboard follows the Mac's.

Completion

onComplete fires once when the last cell fills, with a success haptic and the keyboard dismissed.

Basic
<OtpInput onComplete={verify} />
<OtpInput blurOnComplete={false} haptic={false} onComplete={verify} />

Pattern and keyboard

pattern decides which characters a cell takes, and "digits" shows the number pad.

Letters and digits
<OtpInput length={6} pattern="digits" />
<OtpInput length={8} pattern="alphanumeric" groupSize={4} />
<OtpInput length={6} pattern={/[0-9A-F]/} />

Groups and separators

groupSize splits the row into runs of cells with a short rule between them.

<OtpInput groupSize={3} />
<OtpInput length={8} groupSize={4} />

Write the anatomy out for any other shape.

<OtpInput length={8}>
  <OtpInput.Group>
    <OtpInput.Slot index={0} />
    <OtpInput.Slot index={1} />
    <OtpInput.Slot index={2} />
  </OtpInput.Group>
  <OtpInput.Separator />
  <OtpInput.Group>
    <OtpInput.Slot index={3} />
    <OtpInput.Slot index={4} />
  </OtpInput.Group>
  <OtpInput.Separator />
  <OtpInput.Group>
    <OtpInput.Slot index={5} />
    <OtpInput.Slot index={6} />
    <OtpInput.Slot index={7} />
  </OtpInput.Group>
</OtpInput>

Masking and placeholder

isMasked draws • in filled cells without switching off autofill, and placeholder dims one character into each empty cell.

Masked PIN
<OtpInput isMasked length={4} size="lg" />
<OtpInput placeholder="○" />

Sizes and variants

Input's own axes, so a code field sits level with a text field beside it.

Sizes
Sizes
<OtpInput size="sm" />
<OtpInput size="md" />
<OtpInput size="lg" />
<OtpInput variant="secondary" />

States

Invalid paints every cell destructive and stays so while focused; disabled fades the row and takes no input.

States
States
<OtpInput length={4} placeholder="○" />
<OtpInput defaultValue="4821" length={4} />
<OtpInput defaultValue="4829" isInvalid length={4} />
<OtpInput defaultValue="48" isDisabled length={4} />

In a form

A Field cascades isInvalid and isDisabled, and its label names and focuses the field.

In a field
<Field isInvalid={isWrong}>
  <Field.Label>Verification code</Field.Label>
  <OtpInput groupSize={3} onComplete={verify} />
  <Field.Error>That code is incorrect.</Field.Error>
</Field>

Imperative ref

ref exposes focus(), blur() and clear().

const ref = useRef<OtpInputRef>(null);

ref.current?.clear();
ref.current?.focus();

Accessibility

The row is one element to a screen reader, read as "1 2 3" or, masked, "2 of 4 digits entered".

<OtpInput accessibilityLabel="Card PIN" isMasked length={4} />

API Reference

Anatomy

PartWhat it is
OtpInputOwns the value and the hidden field. With no children, draws the row from length and groupSize
OtpInput.GroupA row of cells
OtpInput.SlotOne cell — its character, the mask or the placeholder, and the caret while active
OtpInput.SeparatorA short rule between groups

OtpInput

Prop

Type

OtpInput.Slot

Prop

Type

useOtpInput

Reads the enclosing field's state — value, length, isFocused, activeIndex, the axes, and slot(index) for one cell's character, fill and caret — for a custom cell.

On this page