The only accessible & unstyled & full featured Input OTP component for Angular
OTP Input for Angular 🔐 by @shhdharmen
Inspired from guilhermerodz/input-otp
Install
ng add @ngxpert/input-otp
Usage
Import the component.
import { InputOTPComponent } from '@ngxpert/input-otp';
@Component({
selector: 'app-my-component',
template: `
<input-otp [maxLength]="6" [(ngModel)]="otpValue" #otpInput>
<div style="display: flex;">
@for (slot of otpInput.slots(); track $index) {
<div>{{ slot.char }}</div>
}
</div>
</input-otp>
`,
imports: [InputOTPComponent, FormsModule],
})
export class MyComponent {
otpValue = '';
}
Features
- ✅ Works with
Template-Driven Forms
andReactive Forms
out of the box. - ✅ Supports copy-paste-cut
- ✅ Supports all keybindings
Default example
The example below uses tailwindcss
tailwind-merge
clsx
. You can see it online here, code available here.
main.component
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { InputOTPComponent } from '@ngxpert/input-otp';
import { SlotComponent } from './slot.component';
import { FakeDashComponent } from './fake-components';
@Component({
selector: 'app-examples-main',
template: `
<input-otp
[maxLength]="6"
containerClass="group flex items-center has-[:disabled]:opacity-30"
[(ngModel)]="otpValue"
#otp="inputOtp"
>
<div class="flex">
@for (
slot of otp.slots().slice(0, 3);
track $index;
let first = $first;
let last = $last
) {
<app-slot
[isActive]="slot.isActive"
[char]="slot.char"
[placeholderChar]="slot.placeholderChar"
[hasFakeCaret]="slot.hasFakeCaret"
[first]="first"
[last]="last"
/>
}
</div>
<app-fake-dash />
<div class="flex">
@for (
slot of otp.slots().slice(3, 6);
track $index + 3;
let last = $last;
let first = $first
) {
<app-slot
[isActive]="slot.isActive"
[char]="slot.char"
[placeholderChar]="slot.placeholderChar"
[hasFakeCaret]="slot.hasFakeCaret"
[first]="first"
[last]="last"
/>
}
</div>
</input-otp>
`,
imports: [FormsModule, InputOTPComponent, SlotComponent, FakeDashComponent],
})
export class ExamplesMainComponent {
otpValue = '';
}
slot.component
import { Component, Input } from '@angular/core';
import { FakeCaretComponent } from './fake-components';
import { cn } from './utils';
@Component({
selector: 'app-slot',
template: `
<div
[class]="
cn(
'relative w-10 h-14 text-[2rem]',
'flex items-center justify-center',
'transition-all duration-300',
'border-y border-r',
'group-hover:border-accent-foreground/20 group-focus-within:border-accent-foreground/20',
'outline outline-0 outline-accent-foreground/20',
{ 'outline-4 outline-accent-foreground': isActive },
{ 'border-l rounded-l-md': first },
{ 'rounded-r-md': last }
)
"
>
@if (char) {
<div>{{ char }}</div>
} @else {
{{ ' ' }}
}
@if (hasFakeCaret) {
<app-fake-caret />
}
</div>
`,
imports: [FakeCaretComponent],
})
export class SlotComponent {
@Input() isActive = false;
@Input() char: string | null = null;
@Input() placeholderChar: string | null = null;
@Input() hasFakeCaret = false;
@Input() first = false;
@Input() last = false;
cn = cn;
}
fake-components
import { Component } from '@angular/core';
@Component({
selector: 'app-fake-dash',
template: `
<div class="flex w-10 justify-center items-center">
<div class="w-3 h-1 rounded-full bg-black/75"></div>
</div>
`,
})
export class FakeDashComponent {}
@Component({
selector: 'app-fake-caret',
template: `
<div
class="absolute pointer-events-none inset-0 flex items-center justify-center animate-caret-blink"
>
<div class="w-[2px] h-8 bg-black/75"></div>
</div>
`,
})
export class FakeCaretComponent {}
utils
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import type { ClassValue } from 'clsx';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
styles
@import "tailwindcss";
@theme {
--animate-caret-blink: caret-blink 1.2s ease-out infinite;
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
}
How it works
There's currently no native OTP/2FA/MFA input in HTML, which means people are either going with
- a simple input design or
- custom designs like this one.
This library works by rendering an invisible input as a sibling of the slots, contained by a relative
ly positioned parent (the container root called input-otp).
API Reference
<input-otp>
The root container. Define settings for the input via inputs. Then, use the inputOtp.slots()
property to create the slots.
Inputs and outputs
export interface InputOTPInputsOutputs {
// The number of slots
maxLength: InputSignal<number>;
// Pro tip: input-otp export some patterns by default such as REGEXP_ONLY_DIGITS which you can import from the same library path
// Example: import { REGEXP_ONLY_DIGITS } from '@ngxpert/input-otp';
// Then use it as: <input-otp [pattern]="REGEXP_ONLY_DIGITS">
pattern?: InputSignal<string | RegExp | undefined>;
// While rendering the input slot, you can access both the char and the placeholder, if there's one and it's active.
// If you expect input to be of 6 characters, provide 6 characters in the placeholder.
placeholder?: InputSignal<string | undefined>;
// Virtual keyboard appearance on mobile
// Default: 'numeric'
inputMode?: InputSignal<'numeric' | 'text'>;
// The autocomplete attribute for the input
// Default: 'one-time-code'
autoComplete?: InputSignal<string | undefined>;
// The class name for the container
containerClass?: InputSignal<string | undefined>;
// Emits the complete value when the input is filled
complete: OutputEmitterRef<string>;
}
Top comments (0)