# PinInput

> A one-time code or PIN field split into cells, with paste, SMS autofill and full keyboard support.

Category: Forms · Status: stable · Package: @bindshq/stance-react@0.1.1 · Docs: https://stanceui.com/components/pin-input

Use PinInput for short codes of a known length: email and SMS verification codes, two-factor codes and PINs. It behaves as one field, so typing advances, Backspace steps back and a pasted or autofilled code fills every cell. For longer or variable-length values use Input.

## Import

```tsx
import { PinInput } from '@bindshq/stance-react';
```

Styles are imported once per app: `@bindshq/stance-tokens/css` and `@bindshq/stance-react/styles.css`.

## Examples

### Verification code

```tsx
import { FormField, PinInput } from '@bindshq/stance-react';

export function Basic() {
  return (
    <FormField
      label="Verification code"
      description="Enter the 6-digit code we sent to priya.nair@binds.si."
    >
      <PinInput name="code" />
    </FormField>
  );
}
```

### Sizes

```tsx
import { PinInput } from '@bindshq/stance-react';

export function Sizes() {
  return (
    <div style={{ display: 'grid', gap: 16 }}>
      <PinInput aria-label="Small code" size="sm" length={4} defaultValue="12" />
      <PinInput aria-label="Medium code" size="md" length={4} defaultValue="12" />
      <PinInput aria-label="Large code" size="lg" length={4} defaultValue="12" />
    </div>
  );
}
```

### Masked PIN

mask hides the characters, for PINs entered in view of others.

```tsx
import { FormField, PinInput } from '@bindshq/stance-react';

export function Masked() {
  return (
    <FormField label="Signing PIN" description="Your 4-digit PIN confirms each signature.">
      <PinInput length={4} mask oneTimeCode={false} />
    </FormField>
  );
}
```

### Letters and numbers

```tsx
import { FormField, PinInput } from '@bindshq/stance-react';

export function Alphanumeric() {
  return (
    <FormField label="Recovery code" description="Letters and numbers, 8 characters.">
      <PinInput length={8} type="alphanumeric" size="sm" oneTimeCode={false} />
    </FormField>
  );
}
```

### Validation

onComplete fires once every cell is filled, so the code can be checked straight away.

```tsx
import * as React from 'react';
import { Button, FormField, PinInput } from '@bindshq/stance-react';

export function Validation() {
  const [code, setCode] = React.useState('');
  const [status, setStatus] = React.useState<'idle' | 'wrong' | 'ok'>('idle');
  return (
    <div style={{ display: 'grid', gap: 12, justifyItems: 'start' }}>
      <FormField
        label="Verification code"
        description="Try 246810."
        error={status === 'wrong' ? 'That code is not correct. Check it and try again.' : undefined}
      >
        <PinInput
          value={code}
          onChange={(v) => {
            setCode(v);
            setStatus('idle');
          }}
          onComplete={(v) => setStatus(v === '246810' ? 'ok' : 'wrong')}
        />
      </FormField>
      <p role="status" style={{ margin: 0 }}>
        {status === 'ok' ? 'Email address verified.' : ''}
      </p>
      <Button variant="link" onClick={() => setCode('')}>
        Send a new code
      </Button>
    </div>
  );
}
```

## API reference

| Component | Prop | Type | Default | Description |
| --- | --- | --- | --- | --- |
| PinInput | `length` | `number` | `6` | Number of cells. |
| PinInput | `value` | `string` | None | Controlled value. |
| PinInput | `defaultValue` | `string` | None | Initial value when uncontrolled. |
| PinInput | `onChange` | `(value: string) => void` | None | Called with the joined value on every change. |
| PinInput | `onComplete` | `(value: string) => void` | None | Called once all cells are filled. |
| PinInput | `type` | `'numeric' \| 'alphanumeric'` | `'numeric'` | Accepted characters. Numeric shows the number keypad on phones. |
| PinInput | `mask` | `boolean` | `false` | Hide the characters. |
| PinInput | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Cell size: 32, 40 or 48px. |
| PinInput | `name` | `string` | None | Adds a hidden input carrying the joined value for form submission. |
| PinInput | `oneTimeCode` | `boolean` | `true` | Sets autocomplete="one-time-code" on the first cell for SMS and email code autofill. |
| PinInput | `invalid` | `boolean` | None | Marks the field invalid. Set automatically inside an erroring FormField. |
| PinInput | `disabled` | `boolean` | `false` | Disables every cell. |
| PinInput | `readOnly` | `boolean` | `false` | Shows the value without allowing changes. |
| PinInput | `autoFocus` | `boolean` | `false` | Focus the first cell on mount. |
| PinInput | `getCellLabel` | `(index: number, length: number) => string` | None | Custom accessible name for each cell. |

## Accessibility

- The cells form a group named by the FormField label (or aria-label), with the description and error linked to it.
- Each cell has its own name, such as “Digit 1 of 6”, so screen-reader users always know their position.
- The first cell uses autocomplete="one-time-code" and accepts a whole code, so SMS and password-manager autofill work.
- Pasting a code fills every cell and ignores spaces and dashes. Never block paste on a verification code (WCAG 3.3.8).
- Numeric codes use inputmode="numeric" to show the number keypad without the drawbacks of type="number".
- Clicking the FormField label focuses the first cell.

## Keyboard

| Keys | Action |
| --- | --- |
| `0 to 9` `A to Z` | Enters a character and moves to the next cell. |
| `Backspace` | Clears the current cell, or steps back and clears the previous one. |
| `Delete` | Clears the current cell. |
| `←` `→` | Moves between cells. |
| `Home` `End` | Moves to the first cell or the next empty one. |
| `Ctrl/⌘` `V` | Pastes a whole code. |

## Guidelines

**Do**

- Say where the code was sent and offer a way to resend it.
- Verify on completion, and keep the code in place if it is wrong so it can be corrected.

**Don’t**

- Use it for values of unknown length such as passwords or phone numbers.
- Clear all cells on an error; a single mistyped digit should be quick to fix.

## Related

- https://stanceui.com/components/input
- https://stanceui.com/components/password-input
- https://stanceui.com/components/form-field
