Inputs

OTP Input

A segmented verification-code input — a single invisible input sits over the visual cells, with paste support, an error shake, and a success ripple that turns the cells green.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add otp-input.en

New project or installation trouble? Start here →

OTP Input is a React one-time password and SMS verification code input built with Tailwind CSS and Motion. The cells are purely visual; underneath sits a single native input, so pasting, autofill and deleting all follow normal browser behaviour.

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

6
Active edited usageDefault props
<OTPInput/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

When to use

  • The verification-code step of a sign-in or sign-up flow (SMS or email)
  • Two-factor authentication, entering the 6-digit code from an authenticator app
  • A confirmation step before a payment or password change
  • Alphanumeric invite or redemption codes (with type="alphanumeric")
  • Not a good fit: promo codes of variable length; use Coupon Code Input instead

Installation

npx webberui@latest add https://webberui.com/r/otp-input.en.json

The install also writes the --wb-duration-fast and --wb-ease-out CSS variables into your global stylesheet.

Usage

import { OTPInput } from "@/components/ui/otp-input";

<OTPInput
  onComplete={(code) => verify(code)}
  error={isError}
  success={isSuccess}
/>

When value is omitted, the component manages it internally; when you need to clear and start over after a failed verification, use controlled mode instead (value + onChange).

Examples

Clear and retry after a failed check

Controlled mode wired to an API: the cells turn green on success, and on failure the row shakes, then clears after 0.6 seconds so the user can try again.

"use client";

import * as React from "react";
import { OTPInput } from "@/components/ui/otp-input";

export function VerifyCodeForm() {
  const [code, setCode] = React.useState("");
  const [error, setError] = React.useState(false);
  const [success, setSuccess] = React.useState(false);

  async function handleComplete(value: string) {
    const res = await fetch("/api/verify-code", {
      method: "POST",
      body: JSON.stringify({ code: value }),
    });
    if (res.ok) {
      setSuccess(true);
      return;
    }
    setError(true);
    setTimeout(() => {
      setCode("");
      setError(false);
    }, 600);
  }

  return (
    <OTPInput
      value={code}
      onChange={(next) => {
        setCode(next);
        setError(false);
      }}
      onComplete={handleComplete}
      error={error}
      success={success}
    />
  );
}

8-character alphanumeric invite code

type="alphanumeric" strips symbols and upper-cases input; length sets 8 cells.

<OTPInput
  length={8}
  type="alphanumeric"
  onComplete={(code) => redeemInvite(code)}
/>

Disable while submitting

Pass disabled while the verification request is in flight to prevent double submits; className applies to the outer container, which is handy for alignment.

<OTPInput
  length={4}
  disabled={isVerifying}
  onComplete={(code) => startVerify(code)}
  className="mx-auto"
/>

Props

PropTypeDefaultDescription
lengthnumber6Number of cells
valuestring—Controlled value; when omitted, the component manages it internally
onChange(value: string) => void—Fires when the value changes
onComplete(code: string) => void—Fires once length characters have been entered, returning the complete code
errorbooleanfalseError state: the whole row shakes on the x axis and the borders turn red
successbooleanfalseSuccess state: the cells ripple green in sequence
type'numeric' | 'alphanumeric''numeric'Allowed character type; alphanumeric upper-cases automatically
disabledbooleanfalseDisables input

Accessibility

It uses a "single invisible input + visual cells" pattern rather than N separate inputs:

  • Screen readers only ever see one standard text field (aria-label="Verification code"), with no jumping between six fields; the visual cells are all aria-hidden
  • autocomplete="one-time-code": iOS/Android can autofill the code in one tap when the SMS arrives
  • Pasting a complete code works directly, with no need to distribute it cell by cell
  • Keyboard behaviour is entirely native input behaviour: Backspace deletes, and inputMode="numeric" brings up the numeric keypad on mobile
  • On error it sets aria-invalid, so assistive technology can tell the field's state

How it works

  • The active cell shows a breathing caret and a focus ring, and characters pop into the cell from scale 0.5 → 1
  • The success ripple staggers by 60ms per cell, turning the border and text green in sequence
  • When the user has "reduce motion" enabled at the system level, the shake and pop animations are disabled and characters and states appear directly

FAQ

Does it work with the Next.js App Router?

Yes. The file already starts with "use client", so you can import it from a Server Component page as usual.

Does it support SMS autofill on phones?

Yes. The hidden input sets autocomplete="one-time-code", so iOS and Android can fill the code in one tap when the SMS arrives. Pasting a full code also fills every cell at once, with non-digit characters filtered out.

Does onComplete fire in controlled mode?

Yes. It fires whenever the value's length goes from below length to exactly length, whether the user typed it or you filled value from outside.

How do I change the cell colours and size?

The cell size (h-12 w-10), borders and error/success colours are Tailwind classes in the source, so edit otp-input.tsx directly. className only applies to the outer container.

Does it respect reduced motion?

Yes. When the system "reduce motion" setting is on, the error shake, the character pop and the success ripple scale are disabled, and the caret stops blinking.