Unified UI

PinInput

A production-ready OTP / PIN / verification code input with auto-advance, paste support, shake-on-error animation, and masked mode.

Basic

A verification code input with per-cell auto-advance, paste distribution, backspace navigation, shake-on-error, and pop animation on digit entry.

Installation

Install the component via the CLI in one command.

npx @work-rjkashyap/unified-ui add pin-input
pnpm dlx @work-rjkashyap/unified-ui add pin-input
npx @work-rjkashyap/unified-ui add pin-input
bunx @work-rjkashyap/unified-ui add pin-input

If you haven't initialized your project yet, run npx @work-rjkashyap/unified-ui init first. See the CLI docs for details.

Or install the full package

Use this approach if you prefer to install the entire design system as a dependency instead of copying individual components.

npm install @work-rjkashyap/unified-ui
pnpm add @work-rjkashyap/unified-ui
yarn add @work-rjkashyap/unified-ui
bun add @work-rjkashyap/unified-ui

Anatomy

import {
	PinInput,
	Button,
} from "@work-rjkashyap/unified-ui";

Basic Usage

The simplest use case is an uncontrolled 6-cell OTP input. Use onComplete to receive the joined PIN string when all cells are filled.

<PinInput
  length={6}
  onComplete={(pin) => verifyOTP(pin)}
/>

Sizes

SizeCell WidthCell HeightFont Size
sm32px40px16px
md40px48px18px
lg48px56px20px

Variants

Input Types

Control the allowed character set with the type prop.

Numeric (default)
Alphanumeric
Alphabetic

Masked Mode

Set mask to hide entered characters behind a bullet (), suitable for PIN entry.

Error State

Setting error={true} applies danger border styling to all cells and triggers a shakeX animation. Each time error transitions from false to true, the shake re-fires.

Success State

<PinInput length={6} success />

The success state applies a green border to all cells, signaling correct entry.

Controlled

Use value (an array of single characters) and onChange for full programmatic control.

const [cells, setCells] = useState(Array(6).fill(""));

<PinInput
  length={6}
  value={cells}
  onChange={setCells}
  onComplete={(pin) => {
    console.log("Complete PIN:", pin);
  }}
/>;

Auto Focus

Set autoFocus to focus the first cell on mount — useful for modal or inline OTP flows.

<PinInput length={6} autoFocus onComplete={handleVerify} />

Paste Support

Users can paste a code directly into any cell. The component distributes the pasted characters across subsequent cells automatically, respecting the allowed type filter.

{/* Paste "123456" into the first cell and all cells fill instantly */}
<PinInput length={6} type="numeric" onComplete={verifyOTP} />

Complete OTP Flow

A typical OTP verification pattern:

import { useState } from "react";
import { PinInput, Button } from "@work-rjkashyap/unified-ui";

function OTPVerification() {
  const [cells, setCells] = useState(Array(6).fill(""));
  const [error, setError] = useState(false);
  const [loading, setLoading] = useState(false);

  const handleComplete = async (pin: string) => {
    setLoading(true);
    setError(false);
    try {
      const valid = await verifyOTP(pin);
      if (!valid) {
        setError(true);
        setCells(Array(6).fill(""));
      }
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="flex flex-col items-center gap-4">
      <p className="text-sm text-muted-foreground">
        Enter the 6-digit code sent to your phone.
      </p>
      <PinInput
        length={6}
        value={cells}
        onChange={setCells}
        onComplete={handleComplete}
        error={error}
        autoFocus
      />
      {error && (
        <p className="text-xs text-danger">
          Invalid code. Please try again.
        </p>
      )}
      <Button
        variant="ghost"
        size="sm"
        onClick={() => setCells(Array(6).fill(""))}
      >
        Resend code
      </Button>
    </div>
  );
}

Props

PropTypeDefaultDescription
lengthnumber6Number of input cells.
valuestring[]Controlled value array (one character per cell).
defaultValuestring[]Initial value for uncontrolled mode.
onChange(value: string[]) => voidCalled on any cell change with the full array.
onComplete(pin: string) => voidCalled when all cells are filled. Receives the joined PIN string.
onClear() => voidCalled when backspace is pressed on an already-empty first cell.
type"numeric" | "alphanumeric" | "alphabetic""numeric"Allowed character set.
variant"default" | "primary""default"Visual variant for focus ring color.
size"sm" | "md" | "lg""md"Cell size.
maskbooleanfalseShow bullet characters instead of the actual input.
errorbooleanfalseApplies danger styling and triggers the shake animation.
successbooleanfalseApplies success (green) styling to all cells.
disabledbooleanfalseDisables all cells.
autoFocusbooleanfalseAutomatically focuses the first cell on mount.
gapstring"gap-2"Tailwind gap class between cells.
aria-labelstring"PIN Input"Accessible label for the input group container.
classNamestringAdditional CSS classes on the container element.

Keyboard Navigation

KeyBehavior
0–9 / A–ZEnter a character and auto-advance to the next cell.
BackspaceClear current cell, or move to the previous cell if already empty.
DeleteClear the current cell without moving focus.
Arrow LeftMove focus to the previous cell.
Arrow RightMove focus to the next cell.
HomeMove focus to the first cell.
EndMove focus to the last cell.

Motion

  • Pop on entry — A subtle pop scale animation plays on the cell background when a digit is entered.
  • Shake on error — The entire group plays a shakeX animation each time error transitions to true.
  • All animations respect prefers-reduced-motion via useReducedMotion().

Accessibility

  • Each cell is an <input> element with aria-label="Digit N of M".
  • The first cell includes autoComplete="one-time-code" for SMS autofill on mobile.
  • aria-invalid="true" is set on all cells when error is active.
  • The container has an aria-label describing the group.
  • All keyboard navigation is implemented natively without relying on mouse events.
  • inputMode="numeric" is set for numeric type inputs on mobile devices.

Design Tokens

TokenUsage
--inputDefault cell border color
--ringFocus ring color (default)
--primaryFocus ring color (primary)
--dangerError state border and ring
--successSuccess state border
--accentFilled cell background tint
--radius-mdCell border radius
--duration-fastTransition speed

On this page