Skip to content
Suluu

OtpInput

A calm verification field built around one real input. A soft wash travels to the active slot, the caret breathes and advances with the same spring, and a completed code lets that wash rest until you press a slot again to edit.

Preview

Type digits or paste a complete code
Enter the six-digit code

Installation

Registry — recommended

Copies the component into your configured shadcn UI directory and installs Motion.

npx shadcn@latest add https://suluu.site/r/otp-input.json

npm package

For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.

pnpm add suluu motion

Usage

Use the uncontrolled form for local verification flows, or pair value with onValueChange when the application owns the code. The default accessible name is “One-time code” and can be replaced with anaria-label oraria-labelledby.

Registry
import { OtpInput } from "@/components/ui/otp-input"

export function VerificationCode() {
  return (
    <OtpInput
      length={6}
      onComplete={(code) => verify(code)}
    />
  )
}
npm
// app/globals.css
@import "suluu/styles.css";
@source "../node_modules/suluu/dist";

// component.tsx
import { OtpInput } from "suluu/otp-input"

export function VerificationCode() {
  return <OtpInput length={6} onComplete={verify} />
}

Controlled code

Controlled
const [code, setCode] = useState("")

<OtpInput
  aria-label="Verification code"
  length={6}
  onComplete={(completeCode) => verify(completeCode)}
  onValueChange={setCode}
  value={code}
/>

Masked PIN and errors

Masked values use native password semantics and hide every digit immediately. An inline error is linked to the input and announced politely; use invalid alone when your application renders the message elsewhere.

PIN
<OtpInput
  aria-label="Payment PIN"
  error={attemptFailed ? "That PIN was not accepted." : undefined}
  invalid={attemptFailed}
  length={4}
  masked
  name="pin"
/>

In context

A compact masked PIN flow. Enter 0000 to feel the invalid settle, or any other four digits to complete.

Confirm your PIN

Enter the four digits used to secure this workspace.

Your PIN stays masked.

Props

OtpInput also accepts safe native input attributes such as id, name, required, form, and autoComplete. Its ref points to the native input while className and style customize the wrapper.

PropTypeDefaultDescription
lengthnumber6Number of equal digit slots.
valuestringControlled numeric code.
defaultValuestring""Initial uncontrolled code.
onValueChange(value) => voidRuns after a typed, deleted, pasted, or autofilled value change.
onComplete(value) => voidRuns when an interaction takes the code from incomplete to full.
maskedbooleanfalseShows bullets and uses password semantics.
disabledbooleanfalseDisables every input path.
invalidbooleanfalseApplies the invalid visual and ARIA state.
errorReactNodeLinked inline error; providing one also marks the field invalid.
autoFocusbooleanfalseFocuses the native input after mount.
size"sm" | "default" | "lg""default"Digit slot dimensions.
motionIntensity"subtle" | "default" | "expressive""default"Spring character and caret rhythm.
inputModeHTML inputMode"numeric"Mobile keyboard hint.
patternstring"[0-9]*"Native form validation pattern.
classNamestringClass name applied to the complete wrapper.

Theming

Override these variables in your light and dark theme scopes. The registry installs the defaults automatically.

--suluu-otp-backgroundDigit slot surface.
--suluu-otp-foregroundVisible digit color.
--suluu-otp-mutedMasked bullet color.
--suluu-otp-borderResting slot border.
--suluu-otp-ringActive slot border.
--suluu-otp-caretCustom caret color.
--suluu-otp-errorInvalid border and message color.
--suluu-otp-shadowResting slot depth.
--suluu-otp-active-shadowActive slot glow.
--suluu-otp-error-shadowInvalid active-slot glow.

Sizes

All slots stay equal and never wrap. On narrow screens their widths and gaps tighten together while their vertical touch area remains stable.

The wrapper exposes state through data-state, data-size, and boolean data attributes. Individual slots expose active, filled, empty, and index data for local restyling.

Accessibility

One native input owns the value, selection, form behavior, mobile keyboard, paste, and one-time-code autofill. The visual slots are hidden from assistive technology, avoiding the noisy experience of navigating four or six separate textboxes.

Backspace, Delete, selection, and arrow-key movement retain native expectations. With reduced motion, the active wash, digit settle, and error shift become immediate, and the custom caret stays steady instead of breathing.

↑↓ to navigate↵ to openesc to close