# Pin Input (/docs/pin-input)





`PinInput` is a row of single-character cells for one-time codes, PINs, and confirmation tokens, built on Ark UI's PinInput machine. Each cell is a real `<input>`; the machine handles autofocus advance, paste-to-fill across cells, backspace navigation, masking, and `otp` autofill, and a hidden input backs form submission.

```tsx
import { PinInput } from '@42-central/ui-react/pin-input';

<PinInput length={6} otp />
```

The value is a single `string` — the machine speaks `string[]` (one char per cell), but `value` / `defaultValue` take a string and `onChange` / `onComplete` hand one back. Like the rest of the family it carries its own `label`, `description`, `error`, and `required` — no external wrapper needed — and passing `error` flips the cells to the invalid (red) state. Omit them and it still composes cleanly inside a [`Field`](/docs/field).

There's no single root to land a `testId` on — each of the `length` cells is its own `<input>`. So instead every cell gets its own `` `${testId}-${i}` `` (0-indexed). See the [Testing](/docs/testing) guide.

## Playground [#playground]

<story.Playground />

## One-time code [#one-time-code]

`otp` opts the cells into `autocomplete="one-time-code"`, so iOS / Android surface the SMS code above the keyboard and fill every cell with one tap.

<story.Preview variant="One-time code" />

## Masked [#masked]

`mask` hides the entered characters like a password field — for PINs that shouldn't be shoulder-surfed.

<story.Preview variant="Masked" />

## Value type [#value-type]

`type` constrains what each cell accepts: `numeric` (default), `alphanumeric`, or `alphabetic`. Numeric cells also hint the numeric keyboard on mobile.

<story.Preview variant="Alphanumeric" />

## Sizes [#sizes]

Five sizes — `xs` through `xl` — matching the rest of the family. Each cell is a square that scales with the text.

<Preview align="block" code="&#x22;<PinInput size=\&#x22;xs\&#x22; length={4} />\n<PinInput size=\&#x22;sm\&#x22; length={4} />\n<PinInput size=\&#x22;md\&#x22; length={4} />\n<PinInput size=\&#x22;lg\&#x22; length={4} />\n<PinInput size=\&#x22;xl\&#x22; length={4} />&#x22;">
  <div className="flex flex-col items-start gap-3">
    <PinInput size="xs" length="4" />

    <PinInput size="sm" length="4" />

    <PinInput size="md" length="4" />

    <PinInput size="lg" length="4" />

    <PinInput size="xl" length="4" />
  </div>
</Preview>

## Colors [#colors]

Like the rest of the form family, the cells are palette-independent throughout — including the focus halo, which is a fixed `brand-500` per the 42 UI Kit spec. `color` is accepted for consistency with the rest of the kit but tints nothing here. See [Colors](/docs/colors) and [Focus](/docs/focus).

<Preview align="block" code="&#x22;<PinInput color=\&#x22;brand\&#x22; length={4} />\n<PinInput color=\&#x22;green\&#x22; length={4} />\n<PinInput color=\&#x22;purple\&#x22; length={4} />&#x22;">
  <div className="flex flex-col items-start gap-3">
    <PinInput color="brand" length="4" />

    <PinInput color="green" length="4" />

    <PinInput color="purple" length="4" />
  </div>
</Preview>

## Invalid & disabled [#invalid--disabled]

`invalid` swaps every cell's border and ring to red regardless of `color` (and sets `aria-invalid`). `disabled` dims and blocks the cells; `readOnly` keeps full contrast but rejects edits.

<Preview align="block" code="&#x22;<PinInput invalid defaultValue=\&#x22;12\&#x22; length={4} />\n<PinInput disabled defaultValue=\&#x22;1234\&#x22; length={4} />\n<PinInput readOnly defaultValue=\&#x22;1234\&#x22; length={4} />&#x22;">
  <div className="flex flex-col items-start gap-3">
    <PinInput defaultValue="12" length="4" />

    <PinInput defaultValue="1234" length="4" />

    <PinInput defaultValue="1234" length="4" />
  </div>
</Preview>

## With label, description & error [#with-label-description--error]

`label`, `description` (helper text), and `error` are built in via [`Field`](/docs/field) — no external wrapper needed. Passing `error` flips the cells to the invalid (red) state; `required` adds the semantic flag and a red asterisk on the label.

<Preview align="block" code="&#x22;<PinInput length={6} otp label=\&#x22;Verification code\&#x22; description=\&#x22;Enter the 6-digit code from your authenticator app.\&#x22; />\n\n<PinInput length={6} required label=\&#x22;Verification code\&#x22; />\n\n<PinInput length={6} label=\&#x22;Verification code\&#x22; error=\&#x22;That code is incorrect\&#x22; defaultValue=\&#x22;12\&#x22; />&#x22;">
  <div className="flex flex-col items-start gap-5">
    <PinInput length="6" label="Verification code" description="Enter the 6-digit code from your authenticator app." />

    <PinInput length="6" label="Verification code" />

    <PinInput length="6" label="Verification code" error="That code is incorrect" defaultValue="12" />
  </div>
</Preview>

If you omit these props, the cells still compose inside an outer [`Field`](/docs/field), inheriting its `invalid`, `disabled`, and `readOnly` from context — so an `error` on the field flips them red automatically.

## Controlled [#controlled]

Drive the value from state. `onChange` fires on every keystroke with the joined string; `onComplete` fires once every cell is filled.

```tsx
'use client';
import { useState } from 'react';
import { PinInput } from '@42-central/ui-react/pin-input';

export function CodeInput() {
  const [value, setValue] = useState('');
  return (
    <PinInput
      length={6}
      value={value}
      onChange={setValue}
      onComplete={(code) => verify(code)}
    />
  );
}
```

## Forms [#forms]

A `name` wires the hidden input into native `<form>` submission and `FormData` — the value submits as the joined string.

```tsx
<form action={verify}>
  <PinInput name="code" length={6} />
</form>
```

## API [#api]

<TypeTable
  type="{
  value: { type: 'string', description: 'Controlled value — one character per cell.' },
  defaultValue: { type: 'string', description: 'Uncontrolled initial value.' },
  onChange: { type: '(value: string) => void', description: 'Fires with the joined value on every change.' },
  onComplete: { type: '(value: string) => void', description: 'Fires with the joined value once every cell is filled.' },
  length: { type: 'number', description: 'Number of cells to render.' },
  otp: { type: 'boolean', default: 'false', description: 'Use autocomplete=&#x22;one-time-code&#x22; for OTP autofill.' },
  mask: { type: 'boolean', default: 'false', description: 'Mask the entered characters like a password field.' },
  type: { type: '&#x22;numeric&#x22; | &#x22;alphanumeric&#x22; | &#x22;alphabetic&#x22;', default: '&#x22;numeric&#x22;', description: 'Which characters each cell accepts.' },
  placeholder: { type: 'string', description: 'Placeholder shown in each empty cell.' },
  name: { type: 'string', description: 'Name for native form submission.' },
  invalid: { type: 'boolean', default: 'false', description: 'Red border + ring; sets aria-invalid.' },
  disabled: { type: 'boolean', default: 'false', description: 'Disable the cells.' },
  readOnly: { type: 'boolean', default: 'false', description: 'Focusable and full-contrast, but rejects edits.' },
  required: { type: 'boolean', default: 'false', description: 'Required flag + asterisk on the label.' },
  label: { type: 'ReactNode', description: 'Field label above the cells.' },
  description: { type: 'ReactNode', description: 'Field helper text below the cells.' },
  error: { type: 'ReactNode', description: 'Field error; sets the invalid (red) state.' },
  size: { type: '&#x22;xs&#x22; | &#x22;sm&#x22; | &#x22;md&#x22; | &#x22;lg&#x22; | &#x22;xl&#x22;', default: '&#x22;md&#x22;', description: 'Cell size and text.' },
  color: { type: 'Color', default: '&#x22;brand&#x22;', description: 'Focus-ring accent.' },
  classNames: { type: '{ root?, input?, field? }', description: 'Per-part class overrides.' },
  testId: { type: 'string', description: 'No single root to land on — each cell gets its own `${testId}-${i}` (0-indexed) instead. See the Testing guide.' },
}"
/>

## Accessibility [#accessibility]

* Full PinInput keyboard model from Ark: typing advances to the next cell, `Backspace` clears and steps back, `←` / `→` move between cells, and pasting a code fills every cell at once.
* Each cell is labelled (`Pin input 1 of N`, …) and the cells are grouped, so assistive tech announces position and length.
* `invalid` sets `aria-invalid` on the cells. Pass `label` / `description` / `error` for an accessible name and error text via the built-in [`Field`](/docs/field), or compose inside an outer `Field` instead.
* A hidden input backs form submission; pass `name` to include the joined value, and `otp` to enable OS-level one-time-code autofill.
