Input OTP

A one-time code input split into slots, with paste support.

import { … } from '@inlaylab/ivorie/input-otp'
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_INPUT_OTP_IMPORTS } from '@inlaylab/ivorie/input-otp';

@Component({
  selector: 'demo-input-otp',
  imports: [IV_INPUT_OTP_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <iv-input-otp maxLength="6" aria-label="One-time code">
      <iv-input-otp-group>
        <iv-input-otp-slot index="0" />
        <iv-input-otp-slot index="1" />
        <iv-input-otp-slot index="2" />
      </iv-input-otp-group>
      <iv-input-otp-separator />
      <iv-input-otp-group>
        <iv-input-otp-slot index="3" />
        <iv-input-otp-slot index="4" />
        <iv-input-otp-slot index="5" />
      </iv-input-otp-group>
    </iv-input-otp>
  `,
})
export default class InputOtpDemo {}

Installation

CLInpm
npx ivorie add input-otp

One hidden input holds the value. The slots only show it, so typing, paste and autofill work like a normal input.

Usage

import { IV_INPUT_OTP_IMPORTS } from '@inlaylab/ivorie/input-otp';
<iv-input-otp maxLength="4" aria-label="Code" [(ngModel)]="code">
  <iv-input-otp-group>
    <iv-input-otp-slot index="0" />
    <iv-input-otp-slot index="1" />
    <iv-input-otp-slot index="2" />
    <iv-input-otp-slot index="3" />
  </iv-input-otp-group>
</iv-input-otp>

Examples

Pattern

pattern is a regex source. Input that does not match is dropped. REGEXP_ONLY_DIGITS and REGEXP_ONLY_DIGITS_AND_CHARS are exported.

Digits only.

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_INPUT_OTP_IMPORTS, REGEXP_ONLY_DIGITS } from '@inlaylab/ivorie/input-otp';

@Component({
  selector: 'demo-input-otp-pattern',
  imports: [IV_INPUT_OTP_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="flex flex-col items-center gap-2">
      <iv-input-otp maxLength="4" aria-label="PIN" [pattern]="digits" (complete)="done.set($event)">
        <iv-input-otp-group>
          <iv-input-otp-slot index="0" />
          <iv-input-otp-slot index="1" />
          <iv-input-otp-slot index="2" />
          <iv-input-otp-slot index="3" />
        </iv-input-otp-group>
      </iv-input-otp>
      <p class="text-muted-foreground text-sm">{{ done() ? 'Entered ' + done() : 'Digits only.' }}</p>
    </div>
  `,
})
export default class InputOtpPatternDemo {
  readonly digits = REGEXP_ONLY_DIGITS;
  readonly done = signal('');
}

Reactive forms

Value: "" · invalid

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { IV_INPUT_OTP_IMPORTS } from '@inlaylab/ivorie/input-otp';

@Component({
  selector: 'demo-input-otp-reactive-forms',
  imports: [IV_INPUT_OTP_IMPORTS, ReactiveFormsModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="flex flex-col items-center gap-2">
      <iv-input-otp maxLength="6" aria-label="Verification code" [formControl]="code">
        <iv-input-otp-group>
          @for (i of slots; track i) {
            <iv-input-otp-slot [index]="i" />
          }
        </iv-input-otp-group>
      </iv-input-otp>
      <p class="text-muted-foreground text-sm">Value: "{{ code.value }}" · {{ code.valid ? 'valid' : 'invalid' }}</p>
    </div>
  `,
})
export default class InputOtpReactiveFormsDemo {
  readonly slots = [0, 1, 2, 3, 4, 5];
  readonly code = new FormControl('', [Validators.required, Validators.minLength(6)]);
}

API

Every part also accepts class.

iv-input-otp

Name Type Default Description
maxLength number required Number of characters.
pattern string | null null Regex source each value must match.
value string '' Value, when not used with forms.
disabled boolean false Disables the input. Form-disabled works too.
id string | null null Id of the hidden input, for a <label for>.
aria-label / aria-labelledby string | null null Label for the input.

Outputs: valueChange (string), complete (string, once every slot is filled).

Works with ngModel and reactive forms.

iv-input-otp-slot

Name Type Default Description
index number required Which character the slot shows.

Other parts

  • iv-input-otp-group, iv-input-otp-separator.

Accessibility

The real input carries the label and value, so screen readers read it as one text field.