input otp

Accessible one-time password component with copy-paste functionality.

PreviousNext
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { ZardInputOtpImports } from '@/shared/components/input-otp/input-otp.imports';

@Component({
  selector: 'z-demo-input-otp-preview',
  imports: [ZardInputOtpImports, FormsModule],
  template: `
    <z-input-otp [zMaxLength]="6" [(ngModel)]="value">
      <z-input-otp-group>
        <z-input-otp-slot [zIndex]="0" />
        <z-input-otp-slot [zIndex]="1" />
        <z-input-otp-slot [zIndex]="2" />
        <z-input-otp-slot [zIndex]="3" />
        <z-input-otp-slot [zIndex]="4" />
        <z-input-otp-slot [zIndex]="5" />
      </z-input-otp-group>
    </z-input-otp>
  `,
})
export class ZardDemoInputOtpPreviewComponent {
  value = '123456';
}

Installation

Copy
npx zard-cli@latest add input-otp

Usage

import { ZardInputOtpImports } from '@/shared/components/input-otp/input-otp.imports';
Copy
<z-input-otp [zMaxLength]="6">
  <z-input-otp-group>
    <z-input-otp-slot [zIndex]="0" />
    <z-input-otp-slot [zIndex]="1" />
    <z-input-otp-slot [zIndex]="2" />
  </z-input-otp-group>
  <z-input-otp-separator />
  <z-input-otp-group>
    <z-input-otp-slot [zIndex]="3" />
    <z-input-otp-slot [zIndex]="4" />
    <z-input-otp-slot [zIndex]="5" />
  </z-input-otp-group>
</z-input-otp>
Copy

Examples

pattern

Use zPattern to restrict the characters a slot accepts.

import { REGEXP_ONLY_DIGITS_AND_CHARS } from '@/shared/components/input-otp/input-otp.utils';

<z-input-otp [zMaxLength]="6" [zPattern]="REGEXP_ONLY_DIGITS_AND_CHARS" [zIntegerOnly]="false">
  ...
</z-input-otp>
Copy
import { Component } from '@angular/core';

import { ZardFieldImports } from '@/shared/components/field/field.imports';
import { ZardInputOtpImports } from '@/shared/components/input-otp/input-otp.imports';
import { REGEXP_ONLY_DIGITS } from '@/shared/components/input-otp/input-otp.utils';

@Component({
  selector: 'z-demo-input-otp-pattern',
  imports: [ZardInputOtpImports, ZardFieldImports],
  template: `
    <div z-field class="w-fit">
      <label z-field-label for="digits-only">Digits Only</label>
      <z-input-otp id="digits-only" [zMaxLength]="6" [zPattern]="REGEXP_ONLY_DIGITS">
        <z-input-otp-group>
          <z-input-otp-slot [zIndex]="0" />
          <z-input-otp-slot [zIndex]="1" />
          <z-input-otp-slot [zIndex]="2" />
          <z-input-otp-slot [zIndex]="3" />
          <z-input-otp-slot [zIndex]="4" />
          <z-input-otp-slot [zIndex]="5" />
        </z-input-otp-group>
      </z-input-otp>
    </div>
  `,
})
export class ZardDemoInputOtpPatternComponent {
  readonly REGEXP_ONLY_DIGITS = REGEXP_ONLY_DIGITS;
}

separator

Use InputOtpSeparator between groups to split the slots into smaller blocks.

import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardInputOtpImports } from '@/shared/components/input-otp/input-otp.imports';

@Component({
  selector: 'z-demo-input-otp-separator',
  imports: [ZardInputOtpImports],
  template: `
    <z-input-otp [zMaxLength]="6">
      <z-input-otp-group>
        <z-input-otp-slot [zIndex]="0" />
        <z-input-otp-slot [zIndex]="1" />
      </z-input-otp-group>
      <z-input-otp-separator />
      <z-input-otp-group>
        <z-input-otp-slot [zIndex]="2" />
        <z-input-otp-slot [zIndex]="3" />
      </z-input-otp-group>
      <z-input-otp-separator />
      <z-input-otp-group>
        <z-input-otp-slot [zIndex]="4" />
        <z-input-otp-slot [zIndex]="5" />
      </z-input-otp-group>
    </z-input-otp>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoInputOtpSeparatorComponent {}

controlled

Bind the value with ngModel to read and write it from the parent component.

Enter your one-time password.
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { ZardInputOtpImports } from '@/shared/components/input-otp/input-otp.imports';

@Component({
  selector: 'z-demo-input-otp-controlled',
  imports: [ZardInputOtpImports, FormsModule],
  template: `
    <div class="space-y-2">
      <z-input-otp [zMaxLength]="6" [(ngModel)]="value">
        <z-input-otp-group>
          <z-input-otp-slot [zIndex]="0" />
          <z-input-otp-slot [zIndex]="1" />
          <z-input-otp-slot [zIndex]="2" />
          <z-input-otp-slot [zIndex]="3" />
          <z-input-otp-slot [zIndex]="4" />
          <z-input-otp-slot [zIndex]="5" />
        </z-input-otp-group>
      </z-input-otp>
      <div class="text-center text-sm">
        @if (value === '') {
          Enter your one-time password.
        } @else {
          You entered: {{ value }}
        }
      </div>
    </div>
  `,
})
export class ZardDemoInputOtpControlledComponent {
  value = '';
}

disabled

Use the disabled binding to disable every slot at once.

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { ZardInputOtpImports } from '@/shared/components/input-otp/input-otp.imports';

@Component({
  selector: 'z-demo-input-otp-disabled',
  imports: [ZardInputOtpImports, FormsModule],
  template: `
    <z-input-otp id="disabled" [zMaxLength]="6" [(ngModel)]="value" [disabled]="true">
      <z-input-otp-group>
        <z-input-otp-slot [zIndex]="0" />
        <z-input-otp-slot [zIndex]="1" />
        <z-input-otp-slot [zIndex]="2" />
      </z-input-otp-group>
      <z-input-otp-separator />
      <z-input-otp-group>
        <z-input-otp-slot [zIndex]="3" />
        <z-input-otp-slot [zIndex]="4" />
        <z-input-otp-slot [zIndex]="5" />
      </z-input-otp-group>
    </z-input-otp>
  `,
})
export class ZardDemoInputOtpDisabledComponent {
  value = '123456';
}

invalid

Use zInvalid on a slot — or on the whole InputOtp — to mark the value as invalid.

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { ZardInputOtpImports } from '@/shared/components/input-otp/input-otp.imports';

@Component({
  selector: 'z-demo-input-otp-invalid',
  imports: [ZardInputOtpImports, FormsModule],
  template: `
    <z-input-otp [zMaxLength]="6" [(ngModel)]="value">
      <z-input-otp-group>
        <z-input-otp-slot [zIndex]="0" zInvalid />
        <z-input-otp-slot [zIndex]="1" zInvalid />
      </z-input-otp-group>
      <z-input-otp-separator />
      <z-input-otp-group>
        <z-input-otp-slot [zIndex]="2" zInvalid />
        <z-input-otp-slot [zIndex]="3" zInvalid />
      </z-input-otp-group>
      <z-input-otp-separator />
      <z-input-otp-group>
        <z-input-otp-slot [zIndex]="4" zInvalid />
        <z-input-otp-slot [zIndex]="5" zInvalid />
      </z-input-otp-group>
    </z-input-otp>
  `,
})
export class ZardDemoInputOtpInvalidComponent {
  value = '000000';
}

four digits

Use zMaxLength to change how many slots the input holds.

import { Component } from '@angular/core';

import { ZardInputOtpImports } from '@/shared/components/input-otp/input-otp.imports';
import { REGEXP_ONLY_DIGITS } from '@/shared/components/input-otp/input-otp.utils';

@Component({
  selector: 'z-demo-input-otp-four-digits',
  imports: [ZardInputOtpImports],
  template: `
    <z-input-otp [zMaxLength]="4" [zPattern]="REGEXP_ONLY_DIGITS">
      <z-input-otp-group>
        <z-input-otp-slot [zIndex]="0" />
        <z-input-otp-slot [zIndex]="1" />
        <z-input-otp-slot [zIndex]="2" />
        <z-input-otp-slot [zIndex]="3" />
      </z-input-otp-group>
    </z-input-otp>
  `,
})
export class ZardDemoInputOtpFourDigitsComponent {
  readonly REGEXP_ONLY_DIGITS = REGEXP_ONLY_DIGITS;
}

alphanumeric

Pair REGEXP_ONLY_DIGITS_AND_CHARS with [zIntegerOnly]="false" to accept letters too.

import { Component } from '@angular/core';

import { ZardInputOtpImports } from '@/shared/components/input-otp/input-otp.imports';
import { REGEXP_ONLY_DIGITS_AND_CHARS } from '@/shared/components/input-otp/input-otp.utils';

@Component({
  selector: 'z-demo-input-otp-alphanumeric',
  imports: [ZardInputOtpImports],
  template: `
    <z-input-otp [zMaxLength]="6" [zPattern]="REGEXP_ONLY_DIGITS_AND_CHARS" [zIntegerOnly]="false">
      <z-input-otp-group>
        <z-input-otp-slot [zIndex]="0" />
        <z-input-otp-slot [zIndex]="1" />
        <z-input-otp-slot [zIndex]="2" />
      </z-input-otp-group>
      <z-input-otp-separator />
      <z-input-otp-group>
        <z-input-otp-slot [zIndex]="3" />
        <z-input-otp-slot [zIndex]="4" />
        <z-input-otp-slot [zIndex]="5" />
      </z-input-otp-group>
    </z-input-otp>
  `,
})
export class ZardDemoInputOtpAlphanumericComponent {
  readonly REGEXP_ONLY_DIGITS_AND_CHARS = REGEXP_ONLY_DIGITS_AND_CHARS;
}

form

Use formControlName to bind the OTP to a reactive form.

Verify your login Enter the verification code we sent to your email address: m@example.com
Having trouble signing in? Contact support
import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';

import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideRefreshCw } from '@ng-icons/lucide';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardCardImports } from '@/shared/components/card/card.imports';
import { ZardFieldImports } from '@/shared/components/field/field.imports';
import { ZardInputOtpImports } from '@/shared/components/input-otp/input-otp.imports';

const SLOT_CLASSES =
  '[&_[data-slot=input-otp-slot]>input]:h-12 [&_[data-slot=input-otp-slot]>input]:w-11 [&_[data-slot=input-otp-slot]>input]:text-xl';

@Component({
  selector: 'z-demo-input-otp-form',
  imports: [ZardInputOtpImports, ZardFieldImports, ZardCardImports, ZardButtonComponent, ReactiveFormsModule, NgIcon],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <z-card class="mx-auto max-w-md">
        <div z-card-header>
          <z-card-title zTitle="Verify your login" />
          <z-card-description [zDescription]="description" />
          <ng-template #description>
            Enter the verification code we sent to your email address:
            <span class="font-medium">m&#64;example.com</span>
          </ng-template>
        </div>

        <div z-card-content>
          <div z-field>
            <div class="flex items-center justify-between">
              <label z-field-label for="otp-verification">Verification code</label>
              <button z-button type="button" zType="outline" zSize="xs">
                <ng-icon name="lucideRefreshCw" />
                Resend Code
              </button>
            </div>
            <z-input-otp id="otp-verification" [zMaxLength]="6" formControlName="code">
              <z-input-otp-group [class]="slotClasses">
                <z-input-otp-slot [zIndex]="0" />
                <z-input-otp-slot [zIndex]="1" />
                <z-input-otp-slot [zIndex]="2" />
              </z-input-otp-group>
              <z-input-otp-separator class="mx-2" />
              <z-input-otp-group [class]="slotClasses">
                <z-input-otp-slot [zIndex]="3" />
                <z-input-otp-slot [zIndex]="4" />
                <z-input-otp-slot [zIndex]="5" />
              </z-input-otp-group>
            </z-input-otp>
            <p z-field-description>
              <a href="#">I no longer have access to this email address.</a>
            </p>
          </div>
        </div>

        <div z-card-footer>
          <div z-field>
            <button z-button type="submit" class="w-full" [disabled]="form.invalid">Verify</button>
            <div class="text-muted-foreground text-sm">
              Having trouble signing in?
              <a href="#" class="hover:text-primary underline underline-offset-4 transition-colors">Contact support</a>
            </div>
          </div>
        </div>
      </z-card>
    </form>
  `,
  viewProviders: [provideIcons({ lucideRefreshCw })],
})
export class ZardDemoInputOtpFormComponent {
  readonly slotClasses = SLOT_CLASSES;

  readonly form = new FormGroup({
    code: new FormControl('', [Validators.required, Validators.minLength(6)]),
  });

  onSubmit(): void {
    if (this.form.valid) {
      console.log('Verification code:', this.form.value.code);
    }
  }
}

API Reference

z-input-otp, [z-input-otp]Component

Container for a one-time password input. Renders its own slots when none are projected and integrates with Angular forms through ControlValueAccessor.

PropertyDescriptionTypeDefault
[class] Custom CSS classes ClassValue ''
[zMaxLength] Maximum number of characters. Falls back to the projected slot count, or 6 when there are none number undefined
[zPattern] Per-character regex pattern used to validate typed and pasted input string '[0-9]'
[zReadonly] Makes every slot readonly boolean false
[zIntegerOnly] Sets inputmode to numeric and restricts keyboard input to digits boolean true
[zInvalid] Marks every slot as invalid; cascades to projected slots boolean false
[zSize] Size variant; cascades to projected slots and separators 'sm' | 'default' | 'lg' 'default'
(zValueChange) Emitted whenever the value changes string -
(zComplete) Emitted when every slot is filled string -

z-input-otp-signal, [z-input-otp-signal]Component

Drop-in alternative to z-input-otp that implements the signal forms FormValueControl<string> contract. Use it when binding through [formField] from '@angular/forms/signals'. Inherits every input and output from z-input-otp.

PropertyDescriptionTypeDefault
[(value)] Current value; two-way bound by [formField] string ''
[(disabled)] Disabled state; two-way bound by [formField] and mirrors the field's disabled state boolean false

z-input-otp-slot, [z-input-otp-slot]Component

Individual character slot. Displays the character, the active state, and the blinking fake caret while focused.

PropertyDescriptionTypeDefault
[class] Custom CSS classes ClassValue ''
[zIndex] Zero-based position of the slot number required
[zInvalid] Marks this slot as invalid; also inherited from the parent InputOtp boolean false

z-input-otp-group, [z-input-otp-group]Component

Groups slots together so they render as a single connected block.

PropertyDescriptionTypeDefault
[class] Custom CSS classes ClassValue ''

z-input-otp-separator, [z-input-otp-separator]Component

Visual separator rendered between slot groups. Marked aria-hidden.

PropertyDescriptionTypeDefault
[class] Custom CSS classes ClassValue ''
github iconwhatsapp icondiscord iconX icon

Made with in Brazil. Open source and available on GitHub .