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.
Getting Started
Stack
Components
- 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
- Form Field
- Hover Card
- Icon
- Input Group
- Input OTP
- Input
- Item
- Kbd
- Label
- Menubar
- 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>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 | - | - |