Spartans get ready! v1 is coming!
We are very close to our first stable release. Expect more announcements in the coming weeks. v1 was made possible by our partner Zerops.
- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Autocomplete
- Avatar
- Badge
- Breadcrumb
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Dropdown Menu
- Empty
- Field
- Form Field
- Hover Card
- Icon
- Input Group
- Input OTP
- Input
- Item
- Kbd
- Label
- Menubar
- Navigation Menu
- Pagination
- Popover
- Progress
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner (Toast)
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Toggle Group
- Tooltip
Input OTP
Accessible one-time password component.
import { Component } from '@angular/core';
import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
import { HlmInputOtp, HlmInputOtpGroup, HlmInputOtpSeparator, HlmInputOtpSlot } from '@spartan-ng/helm/input-otp';
@Component({
selector: 'spartan-input-otp-preview',
imports: [HlmInputOtp, HlmInputOtpGroup, HlmInputOtpSeparator, HlmInputOtpSlot, BrnInputOtp],
template: `
<brn-input-otp hlmInputOtp maxLength="6" inputClass="disabled:cursor-not-allowed">
<div hlmInputOtpGroup>
<hlm-input-otp-slot index="0" />
<hlm-input-otp-slot index="1" />
<hlm-input-otp-slot index="2" />
</div>
<hlm-input-otp-separator />
<div hlmInputOtpGroup>
<hlm-input-otp-slot index="3" />
<hlm-input-otp-slot index="4" />
<hlm-input-otp-slot index="5" />
</div>
</brn-input-otp>
`,
})
export class InputOtpPreview {}Installation
npx nx g @spartan-ng/cli:ui input-otp
ng g @spartan-ng/cli:ui input-otp
Usage
import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
import {
HlmInputOtp
HlmInputOtpGroup
HlmInputOtpSeparator
HlmInputOtpSlot
} from '@spartan-ng/helm/input-otp';<brn-input-otp hlmInputOtp maxLength="6" inputClass="disabled:cursor-not-allowed">
<div hlmInputOtpGroup>
<hlm-input-otp-slot index="0" />
<hlm-input-otp-slot index="1" />
<hlm-input-otp-slot index="2" />
</div>
<hlm-input-otp-separator />
<div hlmInputOtpGroup>
<hlm-input-otp-slot index="3" />
<hlm-input-otp-slot index="4" />
<hlm-input-otp-slot index="5" />
</div>
</brn-input-otp>Animation
The fake caret animation animate-caret-blink is provided by tw-animate-css , which is included in @spartan-ng/brain/hlm-tailwind-preset.css . Here are three options for adding the animation to your project.
/* 1. default import includes 'tw-animate-css' */
@import '@spartan-ng/brain/hlm-tailwind-preset.css';
/* 2. import 'tw-animate-css' direclty */
@import 'tw-animate-css';
/* 3. add animate-caret-blink animation from 'tw-animate-css' */
@theme inline {
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
} Adjust the animation to your needs by changing duration, easing function or keyframes by overriding the CSS variable --animate-caret-blink or the @keyframes caret-blink in your global styles.
@import '@spartan-ng/brain/hlm-tailwind-preset.css';
/* adjust animation duration */
@theme inline {
- --animate-caret-blink: caret-blink 1.25s ease-out infinite;
+ --animate-caret-blink: caret-blink 2s ease-out infinite;
}Examples
Form
Sync the otp to a form by adding formControlName to brn-input-otp .
import { afterNextRender, Component, computed, inject, type OnDestroy, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
import { HlmButton } from '@spartan-ng/helm/button';
import { HlmInputOtp, HlmInputOtpGroup, HlmInputOtpSlot } from '@spartan-ng/helm/input-otp';
import { HlmToaster } from '@spartan-ng/helm/sonner';
import { toast } from 'ngx-sonner';
@Component({
selector: 'spartan-input-otp-form',
imports: [ReactiveFormsModule, HlmButton, HlmToaster, BrnInputOtp, HlmInputOtp, HlmInputOtpGroup, HlmInputOtpSlot],
template: `
<hlm-toaster />
<form [formGroup]="form" (ngSubmit)="submit()" class="space-y-8">
<brn-input-otp
hlmInputOtp
[maxLength]="maxLength"
inputClass="disabled:cursor-not-allowed"
formControlName="otp"
[transformPaste]="transformPaste"
(completed)="submit()"
>
<div hlmInputOtpGroup>
<hlm-input-otp-slot index="0" />
<hlm-input-otp-slot index="1" />
<hlm-input-otp-slot index="2" />
<hlm-input-otp-slot index="3" />
<hlm-input-otp-slot index="4" />
<hlm-input-otp-slot index="5" />
</div>
</brn-input-otp>
<div class="flex flex-col gap-4">
<button type="submit" hlmBtn [disabled]="form.invalid">Submit</button>
<button type="button" hlmBtn variant="ghost" [disabled]="isResendDisabled()" (click)="resendOtp()">
Resend in {{ countdown() }}s
</button>
</div>
</form>
`,
host: {
class: 'preview flex min-h-[350px] w-full justify-center p-10 items-center',
},
})
export class InputOtpFormExample implements OnDestroy {
private readonly _formBuilder = inject(FormBuilder);
private _intervalId?: NodeJS.Timeout;
public readonly countdown = signal(60);
public readonly isResendDisabled = computed(() => this.countdown() > 0);
public maxLength = 6;
/** Overrides global formatDate */
public transformPaste = (pastedText: string) => pastedText.replaceAll('-', '');
public form = this._formBuilder.group({
otp: [null, [Validators.required, Validators.minLength(this.maxLength), Validators.maxLength(this.maxLength)]],
});
constructor() {
afterNextRender(() => this.startCountdown());
}
submit() {
console.log(this.form.value);
toast('OTP submitted', {
description: `Your OTP ${this.form.value.otp} has been submitted`,
});
}
resendOtp() {
// add your api request here to resend OTP
this.resetCountdown();
}
ngOnDestroy() {
this.stopCountdown();
}
private resetCountdown() {
this.countdown.set(60);
this.startCountdown();
}
private startCountdown() {
this.stopCountdown();
this._intervalId = setInterval(() => {
this.countdown.update((countdown) => Math.max(0, countdown - 1));
if (this.countdown() === 0) {
this.stopCountdown();
}
}, 1000);
}
private stopCountdown() {
if (this._intervalId) {
clearInterval(this._intervalId);
this._intervalId = undefined;
}
}
}Brain API
BrnInputOtpSlot
Selector: brn-input-otp-slot
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| index* (required) | number | - | The index of the slot to render the char or a fake caret |
BrnInputOtp
Selector: brn-input-otp
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| hostStyles | string | position: relative; cursor: text; user-select: none; pointer-events: none; | Styles applied to the host element. |
| inputStyles | string | position: absolute; inset: 0; width: 100%; height: 100%; display: flex; textAlign: left; opacity: 1; color: transparent; pointerEvents: all; background: transparent; caret-color: transparent; border: 0px solid transparent; outline: transparent solid 0px; box-shadow: none; line-height: 1; letter-spacing: -0.5em; font-family: monospace; font-variant-numeric: tabular-nums; | Styles applied to the input element to make it invisible and clickable. |
| containerStyles | string | position: absolute; inset: 0; pointer-events: none; | Styles applied to the container element. |
| disabled | boolean | false | Determine if the date picker is disabled. |
| maxLength* (required) | number | - | The number of slots. |
| inputMode | InputMode | numeric | Virtual keyboard appearance on mobile |
| inputClass | ClassValue | - | - |
| transformPaste | (pastedText: string, maxLength: number) => string | (text) => text | Defines how the pasted text should be transformed before saving to model/form. Allows pasting text which contains extra characters like spaces, dashes, etc. and are longer than the maxLength. "XXX-XXX": (pastedText) => pastedText.replaceAll('-', '') "XXX XXX": (pastedText) => pastedText.replaceAll(/\s+/g, '') |
| value | string | null | null | The value controlling the input |
Outputs
| Prop | Type | Default | Description |
|---|---|---|---|
| valueChange | string | - | Emits when the value changes. |
| completed | string | - | Emitted when the input is complete, triggered through input or paste. |
| valueChanged | string | null | null | The value controlling the input |
Helm API
HlmInputOtpFakeCaret
Selector: hlm-input-otp-fake-caret
HlmInputOtpGroup
Selector: [hlmInputOtpGroup]
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| class | ClassValue | - | - |
HlmInputOtpSeparator
Selector: hlm-input-otp-separator
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| class | ClassValue | inline-flex | - |
HlmInputOtpSlot
Selector: hlm-input-otp-slot
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| class | ClassValue | - | - |
| index* (required) | number | - | The index of the slot to render the char or a fake caret |
HlmInputOtp
Selector: brn-input-otp[hlmInputOtp], brn-input-otp[hlm]
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| class | ClassValue | - | - |