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
| Prop | Type | Default | Description |
|---|---|---|---|
| length | number | 6 | Number of digits. Even lengths of six or more split into two groups. |
| onComplete | (code: string) => void | Called with the full code once every slot is filled. | |
| label | string | "One-time code" | Accessible name. Screen reader only unless showLabel is set. |
| showLabel | boolean | false | Show the label above the field. |
| value, defaultValue, onChange | input-otp props | Controlled or uncontrolled. onChange receives the string, not an event. | |
| disabled | boolean | false | Disables 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