Skip to content
goodtabs UI
BrowseBrowse components

Pin input

A one-time code field with one slot per digit that fills on paste.

npx shadcn@latest add https://ui.goodtabs.co/r/pin-input.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/pin-input

Two groups of three with a separator. Typing advances, paste fills every slot.

Four digits

Enter the 4 digit PIN for this card.

4
2

A shorter code in one group, with a starting value.

With a label and resend

Check your email

We sent a 6 digit code to mira@goodtabs.co.

Controlled value, a visible label, onComplete reporting and a resend link.

Usage

import { PinInput } from "@/components/ui/pin-input"

<PinInput label="Verification code" onComplete={(code) => verify(code)} />
<PinInput length={4} label="Card PIN" showLabel />

Props

PropTypeDefaultDescription
lengthnumber6Number of digits. Even lengths of six or more split into two groups.
onComplete(code: string) => voidCalled with the full code once every slot is filled.
labelstring"One-time code"Accessible name. Screen reader only unless showLabel is set.
showLabelbooleanfalseShow the label above the field.
value, defaultValue, onChangeinput-otp propsControlled or uncontrolled. onChange receives the string, not an event.
disabledbooleanfalseDisables the field and dims the slots.

Source

pin-input/pin-input.tsx
"use client";

import * as React from "react";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@/components/ui/input-otp";
import { cn } from "@/lib/utils";

type PinInputProps = Omit<
  React.ComponentProps<typeof InputOTP>,
  "maxLength" | "render" | "children" | "onComplete" | "pattern"
> & {
  /** Number of digits. Six is split into two groups with a separator. */
  length?: number;
  /** Called with the full code once every slot is filled. */
  onComplete?: (code: string) => void;
  /** Accessible name of the field. Shown above it when showLabel is set, otherwise read by screen readers only. */
  label?: string;
  /** Show the label above the field. */
  showLabel?: boolean;
  /** Class for the outer wrapper. The InputOTP container keeps containerClassName. */
  className?: string;
};

/**
 * A one-time code field: one slot per digit, auto-advance, paste fills every slot.
 * Built on the shadcn input-otp primitive, so value and onChange work as there.
 */
export function PinInput({
  length = 6,
  onComplete,
  label = "One-time code",
  showLabel = false,
  className,
  containerClassName,
  disabled,
  id,
  ...props
}: PinInputProps) {
  const generated = React.useId();
  const inputId = id ?? generated;
  const split = length >= 6 && length % 2 === 0;
  const slots = Array.from({ length }, (_, i) => i);

  const renderSlots = (indices: number[]) => indices.map((i) => <InputOTPSlot key={i} index={i} />);

  return (
    <div data-slot="pin-input" className={cn("flex flex-col items-start gap-2", className)}>
      <label htmlFor={inputId} className={showLabel ? "text-sm font-medium" : "sr-only"}>
        {label}
      </label>
      <InputOTP
        id={inputId}
        maxLength={length}
        pattern={REGEXP_ONLY_DIGITS}
        inputMode="numeric"
        autoComplete="one-time-code"
        onComplete={onComplete}
        disabled={disabled}
        containerClassName={cn("gap-2", containerClassName)}
        {...props}
      >
        {split ? (
          <>
            <InputOTPGroup>{renderSlots(slots.slice(0, length / 2))}</InputOTPGroup>
            <InputOTPSeparator aria-hidden="true" />
            <InputOTPGroup>{renderSlots(slots.slice(length / 2))}</InputOTPGroup>
          </>
        ) : (
          <InputOTPGroup>{renderSlots(slots)}</InputOTPGroup>
        )}
      </InputOTP>
    </div>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/pin-input/prompt.md, or the Copy AI prompt button above.

When to use

  • Email or SMS verification codes and two factor prompts
  • Short numeric PINs where each digit should read on its own
  • Anywhere people paste a code from another app

When not to use

  • Passwords or long secrets: use the password input
  • Codes with letters: digits only, by design, so the numeric keyboard appears on phones
  • More than eight characters, where separate slots stop helping

Agent notes

  • Client component that takes functions: a demo or page passing onComplete or onChange must be a client component too
  • Digits only, inputMode numeric and autoComplete one-time-code, so iOS offers the code from Messages
  • onChange fires with the string on every keystroke; onComplete once when the last slot fills. Clear the value yourself after a failed check
  • Slots are 32px squares from the shadcn input-otp primitive; the separator only appears for even lengths of six or more