---
outline: deep
---

# Input OTP

A single native `<input>` with visual digit cells for one-time passcode entry. The `<l-input-otp>` wrapper renders individual bordered cells over a hidden input that handles keyboard, paste, and autocomplete.

**`<l-input-otp>`** — Progressive Custom Element

## Options

### Default

```html
<label
  for="otp-default"
  class="block mb-1.5 text-sm font-medium"
  >Verification code</label
>
<l-input-otp>
  <input id="otp-default" />
</l-input-otp>
```

### Digit count

Set `--digits` on `<l-input-otp>` to change the number of cells. The element automatically sets `maxlength` and `pattern` on the input.

```html
<label
  for="otp-pin"
  class="block mb-1.5 text-sm font-medium"
  >PIN code</label
>
<l-input-otp style="--digits: 4">
  <input id="otp-pin" />
</l-input-otp>
```

### Separator

Add `separator-after` attribute to insert a visual dash after the specified position (e.g., `separator-after="3"` for a 3-3 grouping).

```html
<label
  for="otp-separator"
  class="block mb-1.5 text-sm font-medium"
  >Verification code</label
>
<l-input-otp separator-after="3">
  <input id="otp-separator" />
</l-input-otp>
```

### Size

Set the `size` attribute on `<l-input-otp>`: `sm`, `md` (default), `lg`.

```html
<div class="flex flex-col gap-4 items-start">
  <l-input-otp size="sm">
    <input aria-label="Small OTP" />
  </l-input-otp>
  <l-input-otp>
    <input aria-label="Medium OTP" />
  </l-input-otp>
  <l-input-otp size="lg">
    <input aria-label="Large OTP" />
  </l-input-otp>
</div>
```

### Custom colors

Override the `--cell-*` properties to retheme. Set `--cell-focus-ring` to a full `box-shadow` value (or `none`) to customize the active state. Target `.l-input-otp-cell` for typography tweaks.

```html
<style>
  #demo-otp-custom {
    --cell-size: 3.5rem;
    --cell-gap: 0.75rem;
    --cell-bg-color: white;
    --cell-border-color: oklch(0.92 0.01 270);
    --cell-border-radius: 1rem;
    --cell-focus-color: oklch(0.55 0.25 275);
    --cell-focus-ring: 0 0 0 4px color-mix(in oklab, var(--cell-focus-color) 22%, transparent);
  }

  #demo-otp-custom .l-input-otp-cell {
    font-family: inherit;
    font-weight: 700;
  }
</style>

<label
  for="otp-custom"
  class="block mb-2 text-sm font-semibold text-slate-700"
  >Label</label
>
<l-input-otp
  id="demo-otp-custom"
  style="--digits: 4"
>
  <input
    id="otp-custom"
    value="4200"
  />
</l-input-otp>
```

### Not defined

Before JS loads (`:not(:defined)`), the real `<input>` stays visible and usable as a single field styled with the cell tokens — the code can be entered even without JavaScript. Width tracks `--digits`, `--cell-size`, and `--cell-gap` so layout doesn't shift on hydration, and the field uses `--cell-bg-color` so custom themes carry over.

```html
<l-input-otp>
  <input />
</l-input-otp>
```

Once upgraded, the custom element replaces the input with its visual cells container.

### Disabled

Native `disabled` attribute.

```html
<l-input-otp>
  <input
    value="384291"
    disabled
    aria-label="Verification code"
  />
</l-input-otp>
```

## Accessibility

### Criteria

- **Role** — Uses a native `<input>` element — built-in textbox semantics
- **Accessible name** — Requires `<label>` or `aria-label` to describe the purpose of the code input
- **Input purpose** — `autocomplete="one-time-code"` identifies the field for browser and password manager autofill
- **Validation** — `pattern` and `maxlength` provide client-side validation; `inputmode="numeric"` triggers the numeric keyboard on mobile
- **Visual cells** — Cell container is `aria-hidden="true"` — screen readers interact with the native input only

### Rules

- Always provide an accessible name via `<label>` or `aria-label`
- `autocomplete="one-time-code"`, `inputmode="numeric"`, `type="text"`, `maxlength`, and `pattern` are set automatically by the custom element

### Keyboard interactions

- `Tab` — Moves focus to/from the input
- `0–9` — Types a digit into the next available position
- `Backspace` — Deletes the last entered digit
- `Ctrl + V / Cmd + V` — Pastes a full code from clipboard

> **Why light DOM?**
>
> The native `<input>` stays a real form control — built-in validation, form submission, and `one-time-code` autofill work without JavaScript, and it's usable before the element upgrades. The visual cells are decorative (`aria-hidden="true"`).

## API reference

### Importing

```css
@import 'luxen-ui/css/input-otp';
```

```js
import 'luxen-ui/input-otp';
```

### Attributes & Properties

- **separator-after**: `number | undefined` — Position after which to insert a visual separator (e.g., 3 for a 3-3 grouping).

### CSS custom properties

Set `--digits` on `<l-input-otp>` to change the digit count.

- `--digits` — Number of digit boxes (default: 6). Must match input's maxlength.
- `--cell-size` — Cell width and height (default: 2.75rem). Font size scales automatically.
- `--cell-gap` — Space between cells (default: 0.5rem).
- `--cell-bg-color` — Cell background color.
- `--cell-border-color` — Cell border color.
- `--cell-border-radius` — Cell border-radius.
- `--cell-focus-color` — Border + ring color of the active (focused) cell.
- `--cell-focus-ring` — `box-shadow` of the active cell ring (defaults to a 1px solid ring; set to `none` to disable).
