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

Verification code

Enter the 6-digit code from your email.

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 previous field.
Moves to the next field.
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.
  • 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.

[data-disabled]

on PinInput, PinInputField

Entry is disabled.
:focus-visible

on PinInputField

The field has keyboard focus.
:placeholder-shown

on PinInputField

The field is still empty when a placeholder is set.

Keep exploring