API reference

Every prop, render prop, data attribute and export, with types.

Exports#

import {
  OTPInput,          // the component
  OTPInputContext,   // render state, for the children API
 
  REGEXP_ONLY_DIGITS,
  REGEXP_ONLY_CHARS,
  REGEXP_ONLY_DIGITS_AND_CHARS,
} from 'input-otp'
 
import type { OTPInputProps, RenderProps, SlotProps } from 'input-otp'

OTPInput#

The only component. It renders the container, your slots and the real input, and forwards every unrecognised prop to that input — so anything you can put on <input> works here, including name, required, autoFocus, onKeyDown, aria-* and data-*.

Own props#

maxLengthrequired

number

How many slots the field has, and the maximum length of the value. The render function receives exactly this many slots.
render

(props: RenderProps) => React.ReactNode

Returns the field's markup from the current slot state. Required unless you pass children and read state from OTPInputContext instead — the two are mutually exclusive at the type level.
children

React.ReactNode

The composition alternative to render. Children are wrapped in an OTPInputContext provider.
value

string

Makes the field controlled. Pair it with onChange, which receives a string rather than an event. Omit both and the component keeps its own state, seeded from defaultValue — though that combination warns in development.
onChange

(newValue: string) => unknown

Receives the new value as a string, not an event — the shape you almost always want. Fires for typing, pasting, cutting and deleting.
onComplete

(...args: any[]) => unknown

Called once when the value transitions from shorter than maxLength to exactly maxLength. Editing a full code and refilling it fires it again; re-rendering with the same full value does not.
pattern

string | RegExp

Rejects any change whose whole new value fails the test — so an invalid keystroke or paste is dropped rather than filtered. Also mirrored onto the input's native pattern attribute. Since 1.4.0 there is no default: anything is allowed until you say otherwise.
placeholder

string

Per-slot placeholder characters, exposed as slot.placeholderChar while the value is empty. Also set as aria-placeholder on the input.
pasteTransformer

(pasted: string) => string

Rewrites clipboard text before it is validated and inserted. The usual job is stripping separators, e.g. pasted => pasted.replaceAll('-', ''). Providing it also enables the library's manual paste path on every platform, not just iOS.
containerClassName

string

Class name for the container element. Keep className for the input itself — they are separate on purpose, and mixing them up is the most common styling mistake.
textAlign

'left' | 'center' | 'right'

= 'left'

Where the invisible text sits inside the input. It does not move your slots; it changes which slot a tap lands on and where iOS anchors its selection bubble. Details.
inputMode

'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url'

= 'numeric'

Which on-screen keyboard mobile browsers offer. Switch to 'text' for alphanumeric codes — a numeric keypad cannot type letters.
pushPasswordManagerStrategy

'increase-width' | 'none'

= 'increase-width'

Whether to reserve clipped width so a password manager badge lands beside the field instead of over the last slot. Full explanation and simulator.
noScriptCSSFallback

string | null

= a built-in stylesheet

CSS injected inside <noscript> to make the input visible and usable when JavaScript never runs. Pass your own string to restyle it, or null to opt out (not recommended).
nonce

string

Applied to the <style> tag the library injects, so a style-src Content-Security-Policy that requires nonces doesn't block it. Only needed under such a CSP.

Render props#

interface RenderProps {
  slots: SlotProps[]
  isFocused: boolean
  isHovering: boolean
}
 
interface SlotProps {
  char: string | null
  placeholderChar: string | null
  isActive: boolean
  hasFakeCaret: boolean
}
slots

SlotProps[]

One entry per slot, always maxLength long.
isFocused

boolean

Whether the real input currently has focus. Useful for a container-level focus ring.
isHovering

boolean

Pointer is over the input, and the field is not disabled.

SlotProps#

char

string | null

The character in this slot, or null if the value hasn't reached it yet.
placeholderChar

string | null

This slot's character from the placeholder prop. Non-null only while the value is completely empty — so char ?? placeholderChar is the whole rendering rule.
isActive

boolean

This slot is inside the current selection (or is the insert position). More than one slot can be active at once when a range is selected.
hasFakeCaret

boolean

True when the slot is active and empty — the one place a blinking caret makes sense. The native caret is transparent, so this is your cue to draw one.

OTPInputContext#

The same RenderProps object, delivered through context instead of a callback. Drop the render prop, pass children, and any descendant can read slot state — which is how shadcn/ui builds <InputOTPSlot index={0} />.

import { OTPInput, OTPInputContext } from 'input-otp'
 
function Field() {
  return (
    <OTPInput maxLength={6} containerClassName="group flex">
      <Slot index={0} />
      <Slot index={1} />
      {/* … */}
    </OTPInput>
  )
}
 
function Slot({ index }: { index: number }) {
  const { slots } = React.useContext(OTPInputContext)
  const { char, isActive, hasFakeCaret } = slots[index]
  // …
}

Data attributes#

The library publishes its state onto the DOM as well as into React, which lets CSS react to it without any prop threading. All three live on the input, so group-has-[…] or a sibling selector reaches them from your slots.

data-input-otp

on the input

Marks the real field. Every rule in the library’s injected stylesheet is scoped to this attribute — and so is anything you want to override.
data-input-otp-container

on the container

The wrapper that takes containerClassName. Also what password manager detection measures against.
data-input-otp-placeholder-shown

on the input

Present while the value is empty. Renamed from data-input-otp-empty in 1.4.0. This is the hook the docs' slot uses to dim placeholder characters: group-has-[input[data-input-otp-placeholder-shown]]:opacity-40.
data-input-otp-mss / data-input-otp-mse

on the input

The mirrored selection start and end. Mostly an internal debugging aid, but readable if you want CSS or an external tool to follow the caret.

Standard input pseudo-classes work too, and are usually the cleanest route to whole-field states: has-[:disabled], has-[:read-only], has-[:invalid], focus-within.

Exported patterns#

Three ready-made patterns, so the common cases don't need a regex literal in your JSX:

export const REGEXP_ONLY_DIGITS = '^\\d+$'
export const REGEXP_ONLY_CHARS = '^[a-zA-Z]+$'
export const REGEXP_ONLY_DIGITS_AND_CHARS = '^[a-zA-Z0-9]+$'
  • REGEXP_ONLY_DIGITS — numeric codes.
  • REGEXP_ONLY_CHARS — letters only.
  • REGEXP_ONLY_DIGITS_AND_CHARS — alphanumeric, the usual choice for backup codes.

Ref#

ref is forwarded to the real <input> — not to the container. So inputRef.current.focus(), .select() and .setSelectionRange() all behave normally, and react-hook-form reaches the real input — see Forms for the register typing caveat.

Default no-JS stylesheet#

For reference, this is what noScriptCSSFallback contains unless you replace it:

[data-input-otp] {
  --nojs-bg: white !important;
  --nojs-fg: black !important;
 
  background-color: var(--nojs-bg) !important;
  color: var(--nojs-fg) !important;
  caret-color: var(--nojs-fg) !important;
  letter-spacing: .25em !important;
  text-align: center !important;
  border: 1px solid var(--nojs-fg) !important;
  border-radius: 4px !important;
  width: 100% !important;
}
@media (prefers-color-scheme: dark) {
  [data-input-otp] {
    --nojs-bg: black !important;
    --nojs-fg: white !important;
  }
}