# OTP

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/otp
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=85-2

```tsx
import { OTP } from '@commenda/pathfinder/otp';

export const OTPDefault = () => (
  <OTP
    label="Verification code"
    description="Enter the 6-digit code from your authenticator app."
  />
);
```


## Installation [#installation]

Import the component in your screen:

```tsx
import { OTP } from '@commenda/pathfinder/otp';
```

Load the stylesheet once in your app root, after host styles:

```tsx
import '@commenda/pathfinder/styles.css';
```

Pathfinder ships precompiled CSS; no Tailwind setup is required. In Next.js App Router, put interactive state and callbacks in a client component. See [installation](/docs/getting-started/installation) for the workspace setup.

## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `value` | `string` | conditional | Keep leading zeros; controlled mode requires `onValueChange` |
| `defaultValue` | `string` | optional | Keep leading zeros; controlled mode requires `onValueChange` |
| `length` | `4 \| 5 \| 6 \| 7 \| 8` | optional | `6` numeric characters |
| `groupConfig` | `{ sizes: readonly number[]; }` | optional | Six digits use `[3, 3]`; other lengths use one group. Sizes must sum to length |
| `onComplete` | `((code: string) => void)` | optional | Fires after an edit completes the code; never on mount or blur |
| `loadingConfig` | `{ isLoading: boolean; label?: string; }` | optional | Locks edits, retains form value, announces verification |
| `label` | `string` | conditional | Visible field label. Supply an accessible name when it is omitted. |
| `hideLabel` | `boolean` | optional | Visually hide the label while preserving its accessible name. |
| `aria-label` | `string` | conditional | Accessible name when the field has no visible label. |
| `required` | `boolean` | conditional | Mark the field as required. See validation guidance for the component. |
| `labelConfig` | `{ optionalLabel: string; help?: never; } \| { help: string; optionalLabel?: never; }` | optional | Choose either `optionalLabel` or `help`, never both. `never` excludes the other field. |
| `description` | `string` | optional | Supporting guidance associated with the field. |
| `errorConfig` | `{ message: string; reveal?: "after-blur" \| "immediate"; }` | optional | Validation message and when it becomes visible. |
| `disableConfig` | `DisableConfig` | optional | Disabled state with an optional explanation. |
| `onValueChange` | `((value: string) => void)` | conditional |  |

### groupConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sizes` | `readonly number[]` | Yes |  |

### loadingConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `isLoading` | `boolean` | Yes |  |
| `label` | `string` | No |  |

### labelConfig fields

Choose one shape; excluded fields cannot be combined.

#### Option 1

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `optionalLabel` | `string` | Yes |  |

Do not supply: `help`.

#### Option 2

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `help` | `string` | Yes |  |

Do not supply: `optionalLabel`.

### errorConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `message` | `string` | Yes |  |
| `reveal` | `"after-blur" \| "immediate"` | No |  |

### disableConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `isDisabled` | `boolean` | Yes |  |
| `reason` | `string` | No |  |

Shared `label`, `hideLabel`, `description`, `required`, `errorConfig`, `disableConfig`, and `readOnly` follow [Input](/docs/components/input). The underlying `input-otp` primitive is private.

## Grouping [#grouping]


```tsx
import { OTP } from '@commenda/pathfinder/otp';

export const OTPGrouping = () => (
  <OTP label="Recovery code" length={8} groupConfig={{ sizes: [4, 4] }} />
);
```


## Verification [#verification]

The application starts verification from `onComplete`, owns pending/error state, and clears its previous server error in `onValueChange`. Do not submit a form automatically from both `onComplete` and a separate input handler. Guard against a previous request completing after the user has edited the code; associate the response with the submitted code or request ID.


```tsx
import { useState } from 'react';

import { OTP } from '@commenda/pathfinder/otp';

export const OTPVerification = () => {
  const [code, setCode] = useState('');
  const [error, setError] = useState<string>();
  const [complete, setComplete] = useState(false);

  return (
    <div style={{ display: 'grid', gap: 12 }}>
      <OTP
        label="Verification code"
        value={code}
        onValueChange={(next) => {
          setCode(next);
          setError(undefined);
          setComplete(false);
        }}
        onComplete={(next) => {
          if (next === '012345') setComplete(true);
          else setError('That code is incorrect. Try again.');
        }}
        errorConfig={
          error ? { message: error, reveal: 'immediate' } : undefined
        }
        description="Demo verification: enter 012345. Your application owns the real request."
      />
      {complete && <p role="status">Code accepted.</p>}
    </div>
  );
};
```


## States [#states]


```tsx
import { OTP } from '@commenda/pathfinder/otp';

export const OTPStates = () => (
  <div style={{ display: 'grid', gap: 20 }}>
    <OTP
      label="Verifying"
      defaultValue="012345"
      loadingConfig={{ isLoading: true }}
    />
    <OTP
      label="Unavailable"
      defaultValue="123456"
      disableConfig={{
        isDisabled: true,
        reason: 'Request a new code before continuing.',
      }}
    />
    <OTP
      label="Invalid code"
      defaultValue="123456"
      errorConfig={{
        message: 'That code has expired. Request a new one.',
        reveal: 'immediate',
      }}
    />
  </div>
);
```


Paste fills the code, Backspace edits the previous slot, and leading zeros remain strings. Read [composite field rules](/docs/components/otp#design-rules) for Figma provenance and accessibility rules.

## Design rules [#design-rules]

Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.

### OTP [#otp]

- **OTP-001:** Default six 36px slots in two groups of three; radius 6, 15px medium digits. Other supported lengths are 4–8.
- **OTP-002:** One accessible native input; numeric string values preserve leading zeros, paste, and one-time-code autocomplete. Slots are presentational.
- **OTP-003:** Completion emits an event. The application verifies the code, handles stale responses, and clears server errors on edit. Figma's auto-submit wording is implemented as `onComplete`, with no implicit form submission.
- **OTP-004:** Loading makes the input read-only and announces verification without dropping its native form value. Disabled fields do not submit.

### Shared rules [#shared-rules]

Labels activate the first input. Descriptions, disabled reasons, and validation messages stay associated with the field. Error icons center on the first 18px text line, including wrapped messages. Controlled values require callbacks; uncontrolled form resets restore defaults. Public props expose application values, never private primitive or calendar objects. Styles ship precompiled for React 18/19 and Next.js consumers.
