Skip to content

Fields

Pin Input

A row of one-character fields for entering a short code.

When to use it: Use it for one-time passcodes, PINs, and confirmation codes.

On this page

Example

Loading example…
Pin Input.tsxtsx
import { useState } from "react";
// Integrator: swap for `import { PinInput, PinInputField } from "@comp0/react";`
// once index.ts exports the pin-input barrel.
import { PinInput, PinInputField } from "@comp0/react";

export function Example() {
  const [code, setCode] = useState("");
  const [status, setStatus] = useState("Enter the 6-digit code from your email.");

  return (
    <div className="flex flex-col gap-2">
      <p className="text-base font-medium text-zinc-900 sm:text-sm dark:text-zinc-100">
        Verification code
      </p>
      <PinInput
        aria-label="Verification code"
        className="flex gap-2"
        name="otp"
        value={code}
        onChange={(next) => {
          setCode(next);
          setStatus("Enter the 6-digit code from your email.");
        }}
        onComplete={(next) => setStatus(`Checking ${next}`)}
      >
        {[1, 2, 3, 4, 5, 6].map((digit) => (
          <PinInputField
            key={digit}
            aria-label={`Digit ${digit}`}
            className="size-10 rounded border border-zinc-950/10 bg-white text-center text-lg text-zinc-950 outline-teal-600 focus-visible:outline-2 sm:size-9 sm:text-base dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-50 dark:outline-teal-400"
          />
        ))}
      </PinInput>
      <p className="text-base text-zinc-600 sm:text-sm dark:text-zinc-400">{status}</p>
    </div>
  );
}

Anatomy

Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.

A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.

  1. PinInput

    Group that owns the joined code and moves focus between the fields. Owns a DOM element.

  2. PinInputField

    Native single-character input; its index follows the order the fields mount in. The first field advertises autocomplete="one-time-code". Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Wrap one PinInputField per character in PinInput with an aria-label.

  2. 2

    Add the supporting parts

    Give each field its own aria-label; typing fills and advances, and pasting distributes the whole code.

  3. 3

    Make the behavior clear

    Wire onComplete to submit or verify as soon as the last field fills; pass name for form posts and mask for secret PINs.

    Exampletsx
    <PinInput name="otp" onComplete={verify} aria-label="Verification code">
      <PinInputField aria-label="Digit 1" />
      <PinInputField aria-label="Digit 2" />
      <PinInputField aria-label="Digit 3" />
      <PinInputField aria-label="Digit 4" />
    </PinInput>;

Keyboard

Clears the field, or moves back when it is already empty.
Moves to the field on the visual left.
Moves to the field on the visual right.
Leaves the code; typing a character already advances.

Forms and accessibility

Submits one hidden input named `name` carrying the joined code.

Accessibility checklist

  • Name the group and every field: an aria-label on PinInput and one per PinInputField, such as Digit 1.
  • The first field advertises autocomplete=one-time-code so platforms can offer the received code.
  • Horizontal field arrows mirror in RTL while Backspace still returns to the preceding logical field.
  • Keep a visible way to submit as well; do not rely on onComplete alone to move people forward.

API reference

Importtsx
import { PinInput, PinInputField } from "@comp0/react";

PinInput

DOM element

Group that owns the joined code and moves focus between the fields.

PropTypeDescription
valuestringControlled joined code.
defaultValuestringInitial joined code.
onChange(value: string) => voidReceives the next joined code.
onComplete(value: string) => voidFires once each time typing or pasting fills every field.
type"numeric" | "alphanumeric"Accepted characters; "numeric" filters to digits and is the default.
maskbooleanRenders the fields as password inputs.
namestringSubmits one hidden input carrying the joined code.
formstringAssociates the hidden value with a form by id.
disabledbooleanDisables every field.
aria-labelstringNames the group for assistive technology; required.

PinInputField

DOM element

Native single-character input; its index follows the order the fields mount in. The first field advertises autocomplete="one-time-code".

PropTypeDescription
aria-labelstringNames this field, such as "Digit 1"; required per field.
classNamestringStyle each cell of the code.

Style hooks

Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.

PinInput

Style hookMeaning
[data-disabled]Entry is disabled.

PinInputField

Style hookMeaning
[data-disabled]Entry is disabled.
:focus-visibleThe field has keyboard focus.
:placeholder-shownThe field is still empty when a placeholder is set.

Keep exploring