Input OTP
A one-time password input component for verification codes and secure authentication. Apply av-input-otp to a container <div> with slot children.
Import
import { AvInputOtpImports } from '@avesra/angular'; Compose with av-label, av-description, and av-field-error. For form-level validation and submission, see Form.
Usage
We've sent a code to a****@gmail.com
Didn't receive a code?
Resendimport { Component } from '@angular/core';
import {
AvInputOtpImports,
AvLabelComponent,
AvLinkImports,
} from '@avesra/angular';
@Component({
selector: 'app-input-otp-basic-demo',
imports: [
AvInputOtpImports,
AvLabelComponent,
AvLinkImports,
],
host: { class: 'flex w-full max-w-[280px] flex-col gap-2' },
template: `<div class="flex flex-col gap-1">
<label av-label>Verify account</label>
<p class="text-sm text-muted">We've sent a code to a****@gmail.com</p>
</div>
<div av-input-otp [maxLength]="6">
<div av-input-otp-group>
<div av-input-otp-slot [index]="0"></div>
<div av-input-otp-slot [index]="1"></div>
<div av-input-otp-slot [index]="2"></div>
</div>
<div av-input-otp-separator></div>
<div av-input-otp-group>
<div av-input-otp-slot [index]="3"></div>
<div av-input-otp-slot [index]="4"></div>
<div av-input-otp-slot [index]="5"></div>
</div>
</div>
<div class="flex items-center gap-[5px] px-1 pt-1">
<p class="text-sm text-muted">Didn't receive a code?</p>
<a av-link class="text-foreground" underline="always" href="#" (click)="$event.preventDefault()">Resend</a>
</div>`,
})
export class InputOtpBasicDemo {}Anatomy
Import the Input OTP parts and compose them with attribute selectors on <div> hosts.
<div av-input-otp [maxLength]="6">
<div av-input-otp-group>
<div av-input-otp-slot [index]="0"></div>
<div av-input-otp-slot [index]="1"></div>
<!-- ...rest of the slots -->
</div>
<div av-input-otp-separator></div>
<div av-input-otp-group>
<div av-input-otp-slot [index]="3"></div>
<!-- ...rest of the slots -->
</div>
</div> Input OTP uses a single hidden native input overlaid on visual slot elements. Each slot requires a unique zero-based [index].
Variants
The Input OTP component supports two visual variants:
primary(default) — Standard styling with shadow, suitable for most use casessecondary— Lower emphasis variant without shadow, suitable for use in Surface components
import { Component } from '@angular/core';
import {
AvInputOtpImports,
AvLabelComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-otp-variants-demo',
imports: [
AvInputOtpImports,
AvLabelComponent,
],
host: { class: 'flex w-full max-w-[280px] flex-col gap-6' },
template: `<div class="flex flex-col gap-2">
<label av-label>Primary variant</label>
<div av-input-otp variant="primary" [maxLength]="6">
<div av-input-otp-group>
<div av-input-otp-slot [index]="0"></div>
<div av-input-otp-slot [index]="1"></div>
<div av-input-otp-slot [index]="2"></div>
</div>
<div av-input-otp-separator></div>
<div av-input-otp-group>
<div av-input-otp-slot [index]="3"></div>
<div av-input-otp-slot [index]="4"></div>
<div av-input-otp-slot [index]="5"></div>
</div>
</div>
</div>
<div class="flex flex-col gap-2">
<label av-label>Secondary variant</label>
<div av-input-otp variant="secondary" [maxLength]="6">
<div av-input-otp-group>
<div av-input-otp-slot [index]="0"></div>
<div av-input-otp-slot [index]="1"></div>
<div av-input-otp-slot [index]="2"></div>
</div>
<div av-input-otp-separator></div>
<div av-input-otp-group>
<div av-input-otp-slot [index]="3"></div>
<div av-input-otp-slot [index]="4"></div>
<div av-input-otp-slot [index]="5"></div>
</div>
</div>
</div>`,
})
export class InputOtpVariantsDemo {}In Surface
When used inside a Surface component, use variant="secondary" to apply the lower emphasis variant suitable for surface backgrounds.
import { Component } from '@angular/core';
import {
AvInputOtpImports,
AvLabelComponent,
AvLinkImports,
AvSurfaceComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-otp-on-surface-demo',
imports: [
AvInputOtpImports,
AvLabelComponent,
AvLinkImports,
AvSurfaceComponent,
],
host: { class: 'flex w-full flex-col gap-2' },
template: `<div av-surface class="flex w-full flex-col gap-2 rounded-3xl p-6">
<div class="flex flex-col gap-1">
<label av-label>Verify account</label>
<p class="text-sm text-muted">We've sent a code to a****@gmail.com</p>
</div>
<div av-input-otp variant="secondary" [maxLength]="6">
<div av-input-otp-group>
<div av-input-otp-slot [index]="0"></div>
<div av-input-otp-slot [index]="1"></div>
<div av-input-otp-slot [index]="2"></div>
</div>
<div av-input-otp-separator></div>
<div av-input-otp-group>
<div av-input-otp-slot [index]="3"></div>
<div av-input-otp-slot [index]="4"></div>
<div av-input-otp-slot [index]="5"></div>
</div>
</div>
<div class="flex items-center gap-[5px] px-1 pt-1">
<p class="text-sm text-muted">Didn't receive a code?</p>
<a av-link class="text-foreground" underline="always" href="#" (click)="$event.preventDefault()">Resend</a>
</div>
</div>`,
})
export class InputOtpOnSurfaceDemo {}Disabled State
Set disabled on the root to prevent input and apply disabled styling to slots. Pair with a disabled av-label when the whole field is inactive.
Code verification is currently disabled
import { Component } from '@angular/core';
import {
AvDescriptionComponent,
AvInputOtpImports,
AvLabelComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-otp-disabled-demo',
imports: [
AvInputOtpImports,
AvLabelComponent,
AvDescriptionComponent,
],
host: { class: 'flex w-full max-w-[280px] flex-col gap-2' },
template: `<label av-label disabled>Verify account</label>
<p av-description>Code verification is currently disabled</p>
<div av-input-otp disabled [maxLength]="6">
<div av-input-otp-group>
<div av-input-otp-slot [index]="0"></div>
<div av-input-otp-slot [index]="1"></div>
<div av-input-otp-slot [index]="2"></div>
</div>
<div av-input-otp-separator></div>
<div av-input-otp-group>
<div av-input-otp-slot [index]="3"></div>
<div av-input-otp-slot [index]="4"></div>
<div av-input-otp-slot [index]="5"></div>
</div>
</div>`,
})
export class InputOtpDisabledDemo {}Four Digits
import { Component } from '@angular/core';
import {
AvInputOtpImports,
AvLabelComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-otp-four-digits-demo',
imports: [AvInputOtpImports, AvLabelComponent],
host: { class: 'flex w-full max-w-[280px] flex-col gap-2' },
template: `<label av-label>Enter PIN</label>
<div av-input-otp [maxLength]="4">
<div av-input-otp-group>
<div av-input-otp-slot [index]="0"></div>
<div av-input-otp-slot [index]="1"></div>
<div av-input-otp-slot [index]="2"></div>
<div av-input-otp-slot [index]="3"></div>
</div>
</div>`,
})
export class InputOtpFourDigitsDemo {}Controlled
Control the value with [(value)] to synchronize with state, clear the input, or implement custom validation. For Angular forms, bind with [(ngModel)] (FormsModule) or formControlName / [formControl] (ReactiveFormsModule) — Input OTP implements ControlValueAccessor.
Enter a 6-digit code
import { Component, signal } from '@angular/core';
import {
AvDescriptionComponent,
AvInputOtpImports,
AvLabelComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-otp-controlled-demo',
imports: [
AvInputOtpImports,
AvLabelComponent,
AvDescriptionComponent,
],
host: { class: 'flex w-full max-w-[280px] flex-col gap-2' },
template: `<label av-label>Verify account</label>
<div av-input-otp [maxLength]="6" [(value)]="value">
<div av-input-otp-group>
<div av-input-otp-slot [index]="0"></div>
<div av-input-otp-slot [index]="1"></div>
<div av-input-otp-slot [index]="2"></div>
</div>
<div av-input-otp-separator></div>
<div av-input-otp-group>
<div av-input-otp-slot [index]="3"></div>
<div av-input-otp-slot [index]="4"></div>
<div av-input-otp-slot [index]="5"></div>
</div>
</div>
<p av-description>
@if (value().length > 0) {
Value: {{ value() }} ({{ value().length }}/6) •
<button type="button" class="font-medium text-foreground underline" (click)="clear()">Clear</button>
} @else {
Enter a 6-digit code
}
</p>`,
})
export class InputOtpControlledDemo {
value = signal('');
clear(): void {
this.value.set('');
}
}On Complete
Use the (complete) output to trigger actions when all slots are filled.
import { Component, signal } from '@angular/core';
import {
AvButtonComponent,
AvFormComponent,
AvInputOtpImports,
AvLabelComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-otp-on-complete-demo',
imports: [
AvInputOtpImports,
AvLabelComponent,
AvFormComponent,
AvButtonComponent,
],
host: { class: 'flex w-full justify-center' },
template: `<form av-form class="flex w-full max-w-[280px] flex-col gap-2" (submit)="onSubmit($event)">
<label av-label>Verify account</label>
<div
av-input-otp
[maxLength]="6"
[(value)]="value"
(valueChange)="onChange($event)"
(complete)="onComplete($event)"
>
<div av-input-otp-group>
<div av-input-otp-slot [index]="0"></div>
<div av-input-otp-slot [index]="1"></div>
<div av-input-otp-slot [index]="2"></div>
</div>
<div av-input-otp-separator></div>
<div av-input-otp-group>
<div av-input-otp-slot [index]="3"></div>
<div av-input-otp-slot [index]="4"></div>
<div av-input-otp-slot [index]="5"></div>
</div>
</div>
<button
av-button
class="mt-2 w-full"
type="submit"
variant="primary"
[disabled]="!isComplete()"
[pending]="isSubmitting()"
>
@if (isSubmitting()) {
Verifying...
} @else {
Verify Code
}
</button>
</form>`,
})
export class InputOtpOnCompleteDemo {
value = signal('');
isComplete = signal(false);
isSubmitting = signal(false);
onComplete(_code: string): void {
this.isComplete.set(true);
}
onChange(value: string): void {
this.value.set(value);
this.isComplete.set(false);
}
onSubmit(event: Event): void {
event.preventDefault();
this.isSubmitting.set(true);
setTimeout(() => {
this.isSubmitting.set(false);
this.value.set('');
this.isComplete.set(false);
}, 2000);
}
}Form Example
A complete two-factor authentication form with validation and submission.
import { Component, signal } from '@angular/core';
import {
AvButtonComponent,
AvDescriptionComponent,
AvFieldErrorComponent,
AvFormComponent,
AvInputOtpImports,
AvLabelComponent,
AvLinkImports,
} from '@avesra/angular';
@Component({
selector: 'app-input-otp-form-example-demo',
imports: [
AvInputOtpImports,
AvLabelComponent,
AvDescriptionComponent,
AvFieldErrorComponent,
AvFormComponent,
AvButtonComponent,
AvLinkImports,
],
host: { class: 'flex w-full max-w-[280px] flex-col gap-1' },
template: `<form av-form class="flex flex-col gap-4" (submit)="onSubmit($event)">
<div class="flex flex-col gap-2">
<label av-label>Two-factor authentication</label>
<p av-description>Enter the 6-digit code from your authenticator app</p>
<div
av-input-otp
[maxLength]="6"
[invalid]="!!error()"
[(value)]="value"
(valueChange)="onChange($event)"
>
<div av-input-otp-group>
<div av-input-otp-slot [index]="0"></div>
<div av-input-otp-slot [index]="1"></div>
<div av-input-otp-slot [index]="2"></div>
</div>
<div av-input-otp-separator></div>
<div av-input-otp-group>
<div av-input-otp-slot [index]="3"></div>
<div av-input-otp-slot [index]="4"></div>
<div av-input-otp-slot [index]="5"></div>
</div>
</div>
<p av-field-error id="code-error" [visible]="!!error()">{{ error() }}</p>
</div>
<button
av-button
type="submit"
full-width
variant="primary"
[disabled]="value().length !== 6"
[pending]="isSubmitting()"
>
@if (isSubmitting()) {
Verifying...
} @else {
Verify
}
</button>
<div class="flex items-center justify-center gap-1">
<p class="text-sm text-muted">Having trouble?</p>
<a av-link class="text-sm text-foreground" underline="always" href="#" (click)="$event.preventDefault()">
Use backup code
</a>
</div>
</form>`,
})
export class InputOtpFormExampleDemo {
value = signal('');
error = signal('');
isSubmitting = signal(false);
onChange(value: string): void {
this.value.set(value);
this.error.set('');
}
onSubmit(event: Event): void {
event.preventDefault();
this.error.set('');
if (this.value().length !== 6) {
this.error.set('Please enter all 6 digits');
return;
}
this.isSubmitting.set(true);
setTimeout(() => {
if (this.value() === '123456') {
this.value.set('');
} else {
this.error.set('Invalid code. Please try again.');
}
this.isSubmitting.set(false);
}, 1500);
}
}With Pattern
Use the pattern input to restrict characters. Avesra exports common patterns like AV_REGEXP_ONLY_CHARS and AV_REGEXP_ONLY_DIGITS.
Only alphabetic characters are allowed
import { Component } from '@angular/core';
import {
AV_REGEXP_ONLY_CHARS,
AvDescriptionComponent,
AvInputOtpImports,
AvLabelComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-otp-with-pattern-demo',
imports: [
AvInputOtpImports,
AvLabelComponent,
AvDescriptionComponent,
],
host: { class: 'flex w-full max-w-[280px] flex-col gap-2' },
template: `<label av-label>Enter code (letters only)</label>
<p av-description>Only alphabetic characters are allowed</p>
<div av-input-otp [maxLength]="6" [pattern]="regexpOnlyChars">
<div av-input-otp-group>
<div av-input-otp-slot [index]="0"></div>
<div av-input-otp-slot [index]="1"></div>
<div av-input-otp-slot [index]="2"></div>
</div>
<div av-input-otp-separator></div>
<div av-input-otp-group>
<div av-input-otp-slot [index]="3"></div>
<div av-input-otp-slot [index]="4"></div>
<div av-input-otp-slot [index]="5"></div>
</div>
</div>`,
})
export class InputOtpWithPatternDemo {
readonly regexpOnlyChars = AV_REGEXP_ONLY_CHARS;
}With Validation
Use invalid together with av-field-error to surface errors.
import { Component, signal } from '@angular/core';
import {
AvButtonComponent,
AvDescriptionComponent,
AvFieldErrorComponent,
AvFormComponent,
AvInputOtpImports,
AvLabelComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-otp-with-validation-demo',
imports: [
AvInputOtpImports,
AvLabelComponent,
AvDescriptionComponent,
AvFieldErrorComponent,
AvFormComponent,
AvButtonComponent,
],
host: { class: 'flex w-full max-w-[280px] flex-col gap-2' },
template: `<form av-form class="flex flex-col gap-2" (submit)="onSubmit($event)">
<label av-label>Verify account</label>
<p av-description>Hint: The code is 123456</p>
<div
av-input-otp
name="code"
[maxLength]="6"
[invalid]="isInvalid()"
[aria-describedby]="isInvalid() ? 'code-error' : undefined"
[(value)]="value"
(valueChange)="onChange($event)"
>
<div av-input-otp-group>
<div av-input-otp-slot [index]="0"></div>
<div av-input-otp-slot [index]="1"></div>
<div av-input-otp-slot [index]="2"></div>
</div>
<div av-input-otp-separator></div>
<div av-input-otp-group>
<div av-input-otp-slot [index]="3"></div>
<div av-input-otp-slot [index]="4"></div>
<div av-input-otp-slot [index]="5"></div>
</div>
</div>
<p av-field-error id="code-error" [visible]="isInvalid()">
Invalid code. Please try again.
</p>
<button av-button type="submit" [disabled]="value().length !== 6">Submit</button>
</form>`,
})
export class InputOtpWithValidationDemo {
value = signal('');
isInvalid = signal(false);
onChange(value: string): void {
this.value.set(value);
this.isInvalid.set(false);
}
onSubmit(event: Event): void {
event.preventDefault();
const form = event.target as HTMLFormElement;
const code = new FormData(form).get('code');
if (code !== '123456') {
this.isInvalid.set(true);
return;
}
this.isInvalid.set(false);
this.value.set('');
alert('Code verified successfully!');
}
}Styling
Passing Tailwind CSS classes
Pass utility classes on the root, groups, slots, and separator. Use container-class for the inner layout container.
import { Component } from '@angular/core';
import {
AvInputOtpImports,
AvLabelComponent,
} from '@avesra/angular';
@Component({
selector: 'app-input-otp-custom-styling-demo',
imports: [
AvInputOtpImports,
AvLabelComponent,
],
host: { class: 'flex w-full max-w-[320px] flex-col gap-2' },
template: `<div class="flex flex-col gap-2">
<label av-label>Enter verification code</label>
<div av-input-otp class="gap-3" container-class="gap-4" [maxLength]="6">
<div av-input-otp-group class="gap-3">
<div av-input-otp-slot class="size-12 rounded-lg border-2 text-lg font-bold" [index]="0"></div>
<div av-input-otp-slot class="size-12 rounded-lg border-2 text-lg font-bold" [index]="1"></div>
<div av-input-otp-slot class="size-12 rounded-lg border-2 text-lg font-bold" [index]="2"></div>
</div>
<div av-input-otp-separator class="bg-border h-1 w-2 rounded-full"></div>
<div av-input-otp-group class="gap-3">
<div av-input-otp-slot class="size-12 rounded-lg border-2 text-lg font-bold" [index]="3"></div>
<div av-input-otp-slot class="size-12 rounded-lg border-2 text-lg font-bold" [index]="4"></div>
<div av-input-otp-slot class="size-12 rounded-lg border-2 text-lg font-bold" [index]="5"></div>
</div>
</div>
</div>`,
})
export class InputOtpCustomStylingDemo {}Customizing the component classes
To customize the Input OTP classes, use the @layer components directive. Learn more.
@layer components {
.av-input-otp {
@apply gap-3;
}
.av-input-otp__slot {
@apply size-12 rounded-xl border-2 font-bold;
}
.av-input-otp__slot[data-active="true"] {
@apply border-accent ring-2 ring-accent/20;
}
.av-input-otp__separator {
@apply h-1 w-2 rounded-full bg-border;
}
}Avesra follows the BEM methodology so component variants and states stay reusable and easy to customize.
CSS Classes
The Input OTP component uses these CSS classes:
Base Classes
.av-input-otp— Base container.av-input-otp__container— Inner slot layout container.av-input-otp__group— Group of slots.av-input-otp__slot— Individual input slot.av-input-otp__slot-value— The character inside a slot.av-input-otp__caret— Blinking caret indicator.av-input-otp__separator— Visual separator between groups.av-input-otp__input— Hidden native input overlay
State Classes
.av-input-otp__slot[data-active="true"]— Currently active slot.av-input-otp__slot[data-filled="true"]— Slot with a character.av-input-otp__slot[data-disabled="true"]— Disabled slot.av-input-otp__slot[data-invalid="true"]— Invalid slot.av-input-otp[data-disabled="true"]— Disabled root
Interactive States
The component supports both CSS pseudo-classes and data attributes:
- Hover:
:hoveror[data-hovered="true"]on slot - Active:
[data-active="true"]on slot (currently focused) - Filled:
[data-filled="true"]on slot (contains a character) - Disabled:
[data-disabled="true"]on root and slots - Invalid:
[data-invalid="true"]on slots
API
Root props for div[av-input-otp]. Group, slot, and separator are composed as sibling attribute hosts. Slots require a zero-based [index]. Compose with av-label, av-description, and av-field-error for field chrome.
| Prop | Type | Default | Description |
|---|---|---|---|
maxLength | number | 6 | Number of input slots / maximum characters. |
value | string | '' | Current OTP value. Supports two-way binding with [(value)]. |
default-value | string | '' | Initial value for uncontrolled usage. |
complete | OutputEmitterRef<string> | — | Emits when all slots are filled. |
variant | 'primary' | 'secondary' | 'primary' | Visual variant. primary has shadow; secondary is lower emphasis for surfaces. |
disabled | boolean | false | Whether the input is disabled. |
invalid | boolean | false | Whether the input is in an invalid state. |
validationErrors | string[] | [] | Server-side or custom validation error messages. |
pattern | string | — | Regex pattern for allowed characters (e.g. AV_REGEXP_ONLY_DIGITS). |
inputmode | 'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url' | 'numeric' | Virtual keyboard type on mobile devices. |
placeholder | string | — | Placeholder character shown in empty active slots. |
textAlign | 'left' | 'center' | 'right' | 'left' | Caret alignment — affects slot click focus position. |
name | string | — | Name attribute for form submission. |
autofocus | boolean | false | Whether to focus the input on mount. |
aria-describedby | string | — | ID of the element that describes the input (e.g. field error). |
container-class | string | — | Additional CSS classes for the inner container. |
input-class | string | — | Additional CSS classes for the hidden input. |
pasteTransformer | (text: string) => string | — | Transform pasted text before filtering (e.g. strip hyphens). |
Exported Patterns
Avesra exports common regex patterns for the pattern input:
import {
AV_REGEXP_ONLY_DIGITS,
AV_REGEXP_ONLY_CHARS,
AV_REGEXP_ONLY_DIGITS_AND_CHARS,
} from '@avesra/angular';
@Component({
// ...
template: `
<div av-input-otp [pattern]="regexpOnlyDigits" [maxLength]="6">
<!-- ... -->
</div>
`,
})
export class Example {
readonly regexpOnlyDigits = AV_REGEXP_ONLY_DIGITS;
}AV_REGEXP_ONLY_DIGITS— Only numeric characters (0-9)AV_REGEXP_ONLY_CHARS— Only alphabetic characters (a-z, A-Z)AV_REGEXP_ONLY_DIGITS_AND_CHARS— Alphanumeric characters (0-9, a-z, A-Z)