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#
maxLengthrequirednumber
- 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
childrenand read state from OTPInputContext instead — the two are mutually exclusive at the type level. childrenReact.ReactNode
- The composition alternative to
render. Children are wrapped in anOTPInputContextprovider. valuestring
- 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 fromdefaultValue— 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
maxLengthto exactlymaxLength. Editing a full code and refilling it fires it again; re-rendering with the same full value does not. patternstring | 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
patternattribute. Since 1.4.0 there is no default: anything is allowed until you say otherwise. placeholderstring
- Per-slot placeholder characters, exposed as
slot.placeholderCharwhile the value is empty. Also set asaria-placeholderon 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. containerClassNamestring
- Class name for the container element. Keep
classNamefor 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.
noScriptCSSFallbackstring | 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, ornullto opt out (not recommended). noncestring
- Applied to the
<style>tag the library injects, so astyle-srcContent-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
}slotsSlotProps[]
- One entry per slot, always
maxLengthlong. isFocusedboolean
- Whether the real input currently has focus. Useful for a container-level focus ring.
isHoveringboolean
- Pointer is over the input, and the field is not
disabled.
SlotProps#
charstring | null
- The character in this slot, or
nullif the value hasn't reached it yet. placeholderCharstring | null
- This slot's character from the
placeholderprop. Non-null only while the value is completely empty — sochar ?? placeholderCharis the whole rendering rule. isActiveboolean
- 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.
hasFakeCaretboolean
- 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-otpon 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-containeron the container
- The wrapper that takes containerClassName. Also what password manager detection measures against.
data-input-otp-placeholder-shownon the input
- Present while the value is empty. Renamed from
data-input-otp-emptyin 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-mseon 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;
}
}