Skip to content
SevenUI
Esc
navigateopen⌘Jpreview
Sign in
On this page

Input OTP

Displays an OTP input field built on the Base UI OTPField primitive.

"use client";

import { useState } from "react";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
  InputOTPSeparator,
} from "@/registry/base/ui/input-otp";

export default function InputOTPDemo() {
  const [value, setValue] = useState("");

  return (
    <div className="flex w-full max-w-sm justify-center">
      <InputOTP
        length={6}
        value={value}
        onValueChange={(newValue: string) => setValue(newValue)}
      >
        <InputOTPGroup>
          {Array.from({ length: 3 }).map((_, index) => (
            <InputOTPSlot
              key={index}
              aria-label={
                index === 0
                  ? undefined
                  : `Digit ${index + 1} of 6`
              }
            />
          ))}
        </InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup>
          {Array.from({ length: 3 }).map((_, index) => {
            const globalIndex = 3 + index;
            return (
              <InputOTPSlot
                key={index}
                aria-label={
                  globalIndex === 0
                    ? undefined
                    : `Digit ${globalIndex + 1} of 6`
                }
              />
            );
          })}
        </InputOTPGroup>
      </InputOTP>
    </div>
  );
}

Installation

npx shadcn@latest add https://sevenui.dev/r/input-otp.json

Usage

"use client";

import { useState } from "react";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
  InputOTPSeparator,
} from "@/components/ui/input-otp";

export default function InputOTPDemo() {
  const [value, setValue] = useState("");

  return (
    <InputOTP length={6} value={value} onValueChange={(v) => setValue(v)}>
      <InputOTPGroup>
        {Array.from({ length: 3 }).map((_, i) => (
          <InputOTPSlot key={i} />
        ))}
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        {Array.from({ length: 3 }).map((_, i) => (
          <InputOTPSlot key={i} />
        ))}
      </InputOTPGroup>
    </InputOTP>
  );
}

Examples

Masked

"use client";

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/registry/base/ui/input-otp";
import { Label } from "@/registry/base/ui/label";

export default function InputOTPMask() {
  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor="input-otp-mask">Verification PIN</Label>
      <InputOTP id="input-otp-mask" length={4} mask defaultValue="1234">
        <InputOTPGroup>
          {Array.from({ length: 4 }).map((_, index) => (
            <InputOTPSlot
              key={index}
              aria-label={`PIN digit ${index + 1} of 4`}
            />
          ))}
        </InputOTPGroup>
      </InputOTP>
    </div>
  );
}

API reference

InputOTP

Extends the Base UI OTP Field Root — all its props apply. No third-party input-otp package required.

Prop Type Default
length number (required)
value / defaultValue string
onValueChange (value: string) => void
onValueComplete (value: string) => void — all slots filled
validationType "numeric" | "alpha" | "alphanumeric" | "none" "numeric"
mask boolean false
autoSubmit boolean — submit the owning form when complete false

InputOTPSlot

Extends the Base UI Input part — each slot is a real single-character input; render exactly length of them. Focus styling replaces the old fake-caret behavior.

InputOTPGroup and InputOTPSeparator

InputOTPGroup (a div) draws the joined slot borders and rounded ends; InputOTPSeparator extends the Base UI Separator and renders a minus icon between groups.

Was this page helpful?