- 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
- Hover Card
- Icon
- Input Group
- Input OTP
- Input
- Item
- Kbd
- Label
- Menubar
- Native Select
- 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
Tabs
A set of layered sections of content—known as tab panels—that are displayed one at a time.
Account
Make changes to your account here. Click save when you're done.
Password
Change your password here. After saving, you'll be logged out.
import { Component } from '@angular/core';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmCardImports } from '@spartan-ng/helm/card';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { HlmLabelImports } from '@spartan-ng/helm/label';
import { HlmTabsImports } from '@spartan-ng/helm/tabs';
@Component({
selector: 'spartan-tabs-preview',
imports: [HlmTabsImports, HlmCardImports, HlmLabelImports, HlmInputImports, HlmButtonImports],
host: {
class: 'block w-full max-w-lg',
},
template: `
<hlm-tabs tab="account" class="w-full">
<hlm-tabs-list aria-label="tabs example">
<button hlmTabsTrigger="account">Account</button>
<button hlmTabsTrigger="password">Password</button>
</hlm-tabs-list>
<div hlmTabsContent="account">
<section hlmCard>
<div hlmCardHeader>
<h3 hlmCardTitle>Account</h3>
<p hlmCardDescription>Make changes to your account here. Click save when you're done.</p>
</div>
<p hlmCardContent>
<label class="my-4 block" hlmLabel>
Name
<input class="mt-1.5 w-full" value="Pedro Duarte" hlmInput />
</label>
<label class="my-4 block" hlmLabel>
Username
<input class="mt-1.5 w-full" placeholder="@peduarte" hlmInput />
</label>
</p>
<div hlmCardFooter>
<button hlmBtn>Save Changes</button>
</div>
</section>
</div>
<div hlmTabsContent="password">
<section hlmCard>
<div hlmCardHeader>
<h3 hlmCardTitle>Password</h3>
<p hlmCardDescription>Change your password here. After saving, you'll be logged out.</p>
</div>
<p hlmCardContent>
<label class="my-4 block" hlmLabel>
Old Password
<input class="mt-1.5 w-full" type="password" hlmInput />
</label>
<label class="my-4 block" hlmLabel>
New Password
<input class="mt-1.5 w-full" type="password" hlmInput />
</label>
</p>
<div hlmCardFooter>
<button hlmBtn>Save Password</button>
</div>
</section>
</div>
</hlm-tabs>
`,
})
export class TabsPreview {}Installation
ng g @spartan-ng/cli:ui tabsnx g @spartan-ng/cli:ui tabsimport { DestroyRef, ElementRef, HostAttributeToken, Injector, PLATFORM_ID, effect, inject, runInInjectionContext } from '@angular/core';
import { clsx, type ClassValue } from 'clsx';
import { isPlatformBrowser } from '@angular/common';
import { twMerge } from 'tailwind-merge';
export function hlm(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Global map to track class managers per element
const elementClassManagers = new WeakMap<HTMLElement, ElementClassManager>();
// Global mutation observer for all elements
let globalObserver: MutationObserver | null = null;
const observedElements = new Set<HTMLElement>();
interface ElementClassManager {
element: HTMLElement;
sources: Map<number, { classes: Set<string>; order: number }>;
baseClasses: Set<string>;
isUpdating: boolean;
nextOrder: number;
hasInitialized: boolean;
restoreRafId: number | null;
/** Transitions are suppressed until the first effect writes correct classes */
transitionsSuppressed: boolean;
/** Original inline transition value to restore after suppression (empty string = none was set) */
previousTransition: string;
/** Original inline transition priority to preserve !important when restoring */
previousTransitionPriority: string;
}
let sourceCounter = 0;
/**
* This function dynamically adds and removes classes for a given element without requiring
* the a class binding (e.g. `[class]="..."`) which may interfere with other class bindings.
*
* 1. This will merge the existing classes on the element with the new classes.
* 2. It will also remove any classes that were previously added by this function but are no longer present in the new classes.
* 3. Multiple calls to this function on the same element will be merged efficiently.
*/
export function classes(computed: () => ClassValue[] | string, options: ClassesOptions = {}) {
runInInjectionContext(options.injector ?? inject(Injector), () => {
const elementRef = options.elementRef ?? inject(ElementRef);
const platformId = inject(PLATFORM_ID);
const destroyRef = inject(DestroyRef);
const baseClasses = inject(new HostAttributeToken('class'), { optional: true });
const element = elementRef.nativeElement;
// Create unique identifier for this source
const sourceId = sourceCounter++;
// Get or create the class manager for this element
let manager = elementClassManagers.get(element);
if (!manager) {
// Initialize base classes from variation (host attribute 'class')
const initialBaseClasses = new Set<string>();
if (baseClasses) {
toClassList(baseClasses).forEach((cls) => initialBaseClasses.add(cls));
}
manager = {
element,
sources: new Map(),
baseClasses: initialBaseClasses,
isUpdating: false,
nextOrder: 0,
hasInitialized: false,
restoreRafId: null,
transitionsSuppressed: false,
previousTransition: '',
previousTransitionPriority: '',
};
elementClassManagers.set(element, manager);
// Setup global observer if needed and register this element
setupGlobalObserver(platformId);
observedElements.add(element);
// Suppress transitions until the first effect writes correct classes and
// the browser has painted them. This prevents CSS transition animations
// during hydration when classes change from SSR state to client state.
if (isPlatformBrowser(platformId)) {
manager.previousTransition = element.style.getPropertyValue('transition');
manager.previousTransitionPriority = element.style.getPropertyPriority('transition');
element.style.setProperty('transition', 'none', 'important');
manager.transitionsSuppressed = true;
}
}
// Assign order once at registration time
const sourceOrder = manager.nextOrder++;
function updateClasses(): void {
// Get the new classes from the computed function
const newClasses = toClassList(computed());
// Update this source's classes, keeping the original order
manager!.sources.set(sourceId, {
classes: new Set(newClasses),
order: sourceOrder,
});
// Update the element
updateElement(manager!);
// Re-enable transitions after the first effect writes correct classes.
// Deferred to next animation frame so the browser paints the class change
// with transitions disabled first, then re-enables them.
if (manager!.transitionsSuppressed) {
manager!.transitionsSuppressed = false;
manager!.restoreRafId = requestAnimationFrame(() => {
manager!.restoreRafId = null;
restoreTransitionSuppression(manager!);
});
}
}
// Register cleanup with DestroyRef
destroyRef.onDestroy(() => {
if (manager!.restoreRafId !== null) {
cancelAnimationFrame(manager!.restoreRafId);
manager!.restoreRafId = null;
}
if (manager!.transitionsSuppressed) {
manager!.transitionsSuppressed = false;
restoreTransitionSuppression(manager!);
}
// Remove this source from the manager
manager!.sources.delete(sourceId);
// If no more sources, clean up the manager
if (manager!.sources.size === 0) {
cleanupManager(element);
} else {
// Update element without this source's classes
updateElement(manager!);
}
});
/**
* We need this effect to track changes to the computed classes. Ideally, we would use
* afterRenderEffect here, but that doesn't run in SSR contexts, so we use a standard
* effect which works in both browser and SSR.
*/
effect(updateClasses);
});
}
function restoreTransitionSuppression(manager: ElementClassManager): void {
const prev = manager.previousTransition;
if (prev) {
manager.element.style.setProperty('transition', prev, manager.previousTransitionPriority || undefined);
} else {
manager.element.style.removeProperty('transition');
}
}
// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types
function setupGlobalObserver(platformId: Object): void {
if (isPlatformBrowser(platformId) && !globalObserver) {
// Create single global observer that watches the entire document
globalObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const element = mutation.target as HTMLElement;
const manager = elementClassManagers.get(element);
// Only process elements we're managing
if (manager && observedElements.has(element)) {
if (manager.isUpdating) continue; // Ignore changes we're making
// Update base classes to include any externally added classes
const currentClasses = toClassList(element.className);
const allSourceClasses = new Set<string>();
// Collect all classes from all sources
for (const source of manager.sources.values()) {
for (const className of source.classes) {
allSourceClasses.add(className);
}
}
// Any classes not from sources become new base classes
manager.baseClasses.clear();
for (const className of currentClasses) {
if (!allSourceClasses.has(className)) {
manager.baseClasses.add(className);
}
}
updateElement(manager);
}
}
}
});
// Start observing the entire document for class attribute changes
globalObserver.observe(document, {
attributes: true,
attributeFilter: ['class'],
subtree: true, // Watch all descendants
});
}
}
function updateElement(manager: ElementClassManager): void {
if (manager.isUpdating) return; // Prevent recursive updates
manager.isUpdating = true;
// Handle initialization: capture base classes after first source registration
if (!manager.hasInitialized && manager.sources.size > 0) {
// Get current classes on element (may include SSR classes)
const currentClasses = toClassList(manager.element.className);
// Get all classes that will be applied by sources
const allSourceClasses = new Set<string>();
for (const source of manager.sources.values()) {
source.classes.forEach((className) => allSourceClasses.add(className));
}
// Only consider classes as "base" if they're not produced by any source
// This prevents SSR-rendered classes from being preserved as base classes
currentClasses.forEach((className) => {
if (!allSourceClasses.has(className)) {
manager.baseClasses.add(className);
}
});
manager.hasInitialized = true;
}
// Get classes from all sources, sorted by registration order (later takes precedence)
const sortedSources = Array.from(manager.sources.entries()).sort(([, a], [, b]) => a.order - b.order);
const allSourceClasses: string[] = [];
for (const [, source] of sortedSources) {
allSourceClasses.push(...source.classes);
}
// Combine base classes with all source classes, ensuring base classes take precedence
const classesToApply =
allSourceClasses.length > 0 || manager.baseClasses.size > 0
? hlm([...allSourceClasses, ...manager.baseClasses])
: '';
// Apply the classes to the element
if (manager.element.className !== classesToApply) {
manager.element.className = classesToApply;
}
manager.isUpdating = false;
}
function cleanupManager(element: HTMLElement): void {
// Remove from global tracking
observedElements.delete(element);
elementClassManagers.delete(element);
// If no more elements being tracked, cleanup global observer
if (observedElements.size === 0 && globalObserver) {
globalObserver.disconnect();
globalObserver = null;
}
}
interface ClassesOptions {
elementRef?: ElementRef<HTMLElement>;
injector?: Injector;
}
// Cache for parsed class lists to avoid repeated string operations
const classListCache = new Map<string, string[]>();
function toClassList(className: string | ClassValue[]): string[] {
// For simple string inputs, use cache to avoid repeated parsing
if (typeof className === 'string' && classListCache.has(className)) {
return classListCache.get(className)!;
}
const result = clsx(className)
.split(' ')
.filter((c) => c.length > 0);
// Cache string results, but limit cache size to prevent memory growth
if (typeof className === 'string' && classListCache.size < 1000) {
classListCache.set(className, result);
}
return result;
}import type, { ClassValue } from 'clsx';
import type, { Observable } from 'rxjs';
import { BrnTabs, BrnTabsContent, BrnTabsContentLazy, BrnTabsList, BrnTabsPaginatedList, BrnTabsTrigger, type BrnPaginatedTabHeaderItem } from '@spartan-ng/brain/tabs';
import { CdkObserveContent } from '@angular/cdk/observers';
import { ChangeDetectionStrategy, Component, Directive, computed, contentChildren, input, viewChild, type ElementRef } from '@angular/core';
import { HlmIcon } from '@spartan-ng/helm/icon';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { buttonVariants } from '@spartan-ng/helm/button';
import { classes, hlm } from '@spartan-ng/helm/utils';
import { cva, type VariantProps } from 'class-variance-authority';
import { lucideChevronLeft, lucideChevronRight } from '@ng-icons/lucide';
import { toObservable } from '@angular/core/rxjs-interop';
@Directive({
selector: 'ng-template[hlmTabsContentLazy]',
hostDirectives: [BrnTabsContentLazy],
})
export class HlmTabsContentLazy {}
@Directive({
selector: '[hlmTabsContent]',
hostDirectives: [{ directive: BrnTabsContent, inputs: ['brnTabsContent: hlmTabsContent'] }],
host: {
'data-slot': 'tabs-content',
},
})
export class HlmTabsContent {
public readonly contentFor = input.required<string>({ alias: 'hlmTabsContent' });
constructor() {
classes(() => 'flex-1 text-sm outline-none');
}
}
export const listVariants = cva(
'group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center rounded-lg p-[3px] group-data-horizontal/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none',
{
variants: {
variant: {
default: 'bg-muted',
line: 'gap-1 bg-transparent',
},
},
defaultVariants: {
variant: 'default',
},
},
);
type ListVariants = VariantProps<typeof listVariants>;
@Directive({
selector: '[hlmTabsList],hlm-tabs-list',
hostDirectives: [BrnTabsList],
host: {
'data-slot': 'tabs-list',
'[attr.data-variant]': 'variant()',
},
})
export class HlmTabsList {
public readonly variant = input<ListVariants['variant']>('default');
constructor() {
classes(() => listVariants({ variant: this.variant() }));
}
}
@Component({
selector: 'hlm-paginated-tabs-list',
imports: [CdkObserveContent, NgIcon, HlmIcon],
providers: [provideIcons({ lucideChevronRight, lucideChevronLeft })],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'data-slot': 'tabs-paginated-list',
},
template: `
<button
#previousPaginator
data-pagination="previous"
type="button"
aria-hidden="true"
tabindex="-1"
[class.flex]="showPaginationControls()"
[class.hidden]="!showPaginationControls()"
[class]="_paginationButtonClass()"
[disabled]="disableScrollBefore || null"
(click)="_handlePaginatorClick('before')"
(mousedown)="_handlePaginatorPress('before', $event)"
(touchend)="_stopInterval()"
>
<ng-icon hlm size="base" name="lucideChevronLeft" />
</button>
<div #tabListContainer class="z-[1] flex grow overflow-hidden" (keydown)="_handleKeydown($event)">
<div class="relative grow transition-transform" #tabList role="tablist" (cdkObserveContent)="_onContentChanges()">
<div #tabListInner [class]="_tabListClass()">
<ng-content />
</div>
</div>
</div>
<button
#nextPaginator
data-pagination="next"
type="button"
aria-hidden="true"
tabindex="-1"
[class.flex]="showPaginationControls()"
[class.hidden]="!showPaginationControls()"
[class]="_paginationButtonClass()"
[disabled]="disableScrollAfter || null"
(click)="_handlePaginatorClick('after')"
(mousedown)="_handlePaginatorPress('after', $event)"
(touchend)="_stopInterval()"
>
<ng-icon hlm size="base" name="lucideChevronRight" />
</button>
`,
})
export class HlmTabsPaginatedList extends BrnTabsPaginatedList {
constructor() {
super();
classes(() => 'relative flex flex-shrink-0 gap-1 overflow-hidden');
}
public readonly items = contentChildren(BrnTabsTrigger, { descendants: false });
/** Explicitly annotating type to avoid non-portable inferred type */
public readonly itemsChanges: Observable<ReadonlyArray<BrnPaginatedTabHeaderItem>> = toObservable(this.items);
public readonly tabListContainer = viewChild.required<ElementRef<HTMLElement>>('tabListContainer');
public readonly tabList = viewChild.required<ElementRef<HTMLElement>>('tabList');
public readonly tabListInner = viewChild.required<ElementRef<HTMLElement>>('tabListInner');
public readonly nextPaginator = viewChild.required<ElementRef<HTMLElement>>('nextPaginator');
public readonly previousPaginator = viewChild.required<ElementRef<HTMLElement>>('previousPaginator');
public readonly tabListClass = input<ClassValue>('', { alias: 'tabListClass' });
protected readonly _tabListClass = computed(() => hlm(listVariants(), this.tabListClass()));
public readonly paginationButtonClass = input<ClassValue>('', { alias: 'paginationButtonClass' });
protected readonly _paginationButtonClass = computed(() =>
hlm(
'relative z-[2] select-none disabled:cursor-default',
buttonVariants({ variant: 'ghost', size: 'icon' }),
this.paginationButtonClass(),
),
);
protected _itemSelected(event: KeyboardEvent) {
event.preventDefault();
}
}
@Directive({
selector: '[hlmTabsTrigger]',
hostDirectives: [{ directive: BrnTabsTrigger, inputs: ['brnTabsTrigger: hlmTabsTrigger', 'disabled'] }],
host: {
'data-slot': 'tabs-trigger',
},
})
export class HlmTabsTrigger {
public readonly triggerFor = input.required<string>({ alias: 'hlmTabsTrigger' });
constructor() {
classes(() => [
`focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_ng-icon]:pointer-events-none [&_ng-icon]:shrink-0 [&_ng-icon:not([class*='text-'])]:text-base`,
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
'data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground',
'after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
]);
}
}
@Directive({
selector: '[hlmTabs],hlm-tabs',
hostDirectives: [
{
directive: BrnTabs,
inputs: ['orientation', 'activationMode', 'brnTabs: tab'],
outputs: ['tabActivated'],
},
],
host: {
'data-slot': 'tabs',
},
})
export class HlmTabs {
public readonly tab = input.required<string>();
constructor() {
classes(() => 'group/tabs flex gap-2 data-[orientation=horizontal]:flex-col');
}
}
export const HlmTabsImports = [
HlmTabs,
HlmTabsList,
HlmTabsTrigger,
HlmTabsContent,
HlmTabsContentLazy,
HlmTabsPaginatedList,
] as const;Usage
import { HlmTabsImports } from '@spartan-ng/helm/tabs';<hlm-tabs tab="account" class="w-full">
<hlm-tabs-list class="grid w-full grid-cols-2" aria-label="tabs example">
<button hlmTabsTrigger="account">Account</button>
<button hlmTabsTrigger="password">Password</button>
</hlm-tabs-list>
<div hlmTabsContent="account">Make your account here</div>
<div hlmTabsContent="password">Change your password here</div>
</hlm-tabs>Examples
Vertical
Account
Make changes to your account here. Click save when you're done.
Password
Change your password here. After saving, you'll be logged out.
Delete Account
Are you sure you want to delete your account? You cannot undo this action.
import { Component } from '@angular/core';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmCardImports } from '@spartan-ng/helm/card';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { HlmLabelImports } from '@spartan-ng/helm/label';
import { HlmTabsImports } from '@spartan-ng/helm/tabs';
@Component({
selector: 'spartan-tabs-vertical',
imports: [HlmTabsImports, HlmCardImports, HlmLabelImports, HlmInputImports, HlmButtonImports],
host: {
class: 'block w-full max-w-lg min-h-[400px]',
},
template: `
<hlm-tabs tab="account" class="mx-auto flex max-w-3xl flex-row space-x-2" orientation="vertical">
<hlm-tabs-list aria-label="tabs example">
<button class="w-full" hlmTabsTrigger="account">Account</button>
<button class="w-full" hlmTabsTrigger="password">Password</button>
<button class="w-full" hlmTabsTrigger="danger">Danger Zone</button>
</hlm-tabs-list>
<div hlmTabsContent="account">
<section hlmCard>
<div hlmCardHeader>
<h3 hlmCardTitle>Account</h3>
<p hlmCardDescription>Make changes to your account here. Click save when you're done.</p>
</div>
<p hlmCardContent>
<label class="my-4 block" hlmLabel>
Name
<input class="mt-1.5 w-full" value="Pedro Duarte" hlmInput />
</label>
<label class="my-4 block" hlmLabel>
Username
<input class="mt-1.5 w-full" placeholder="@peduarte" hlmInput />
</label>
</p>
<div hlmCardFooter>
<button hlmBtn>Save Changes</button>
</div>
</section>
</div>
<div hlmTabsContent="password">
<section hlmCard>
<div hlmCardHeader>
<h3 hlmCardTitle>Password</h3>
<p hlmCardDescription>Change your password here. After saving, you'll be logged out.</p>
</div>
<p hlmCardContent>
<label class="my-4 block" hlmLabel>
Old Password
<input class="mt-1.5 w-full" type="password" hlmInput />
</label>
<label class="my-4 block" hlmLabel>
New Password
<input class="mt-1.5 w-full" type="password" hlmInput />
</label>
</p>
<div hlmCardFooter>
<button hlmBtn>Save Password</button>
</div>
</section>
</div>
<div hlmTabsContent="danger">
<section hlmCard>
<div hlmCardHeader>
<h3 hlmCardTitle>Delete Account</h3>
<p hlmCardDescription>Are you sure you want to delete your account? You cannot undo this action.</p>
</div>
<div hlmCardFooter>
<button variant="destructive" hlmBtn>Delete Account</button>
</div>
</section>
</div>
</hlm-tabs>
`,
})
export class TabsVerticalPreview {}Basic
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { HlmTabsImports } from '@spartan-ng/helm/tabs';
@Component({
selector: 'spartan-tabs-basic',
imports: [HlmTabsImports],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<hlm-tabs tab="home">
<hlm-tabs-list>
<button hlmTabsTrigger="home">Home</button>
<button hlmTabsTrigger="settings">Settings</button>
</hlm-tabs-list>
</hlm-tabs>
`,
})
export class TabsBasicPreview {}Line
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { HlmTabsImports } from '@spartan-ng/helm/tabs';
@Component({
selector: 'spartan-tabs-line',
imports: [HlmTabsImports],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<hlm-tabs tab="home">
<hlm-tabs-list variant="line">
<button hlmTabsTrigger="home">Home</button>
<button hlmTabsTrigger="settings">Settings</button>
</hlm-tabs-list>
</hlm-tabs>
`,
})
export class TabsLinePreview {}With Icons
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideAppWindow, lucideCode } from '@ng-icons/lucide';
import { HlmTabsImports } from '@spartan-ng/helm/tabs';
@Component({
selector: 'spartan-tabs-with-icons',
imports: [HlmTabsImports, NgIcon],
providers: [provideIcons({ lucideAppWindow, lucideCode })],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<hlm-tabs tab="preview">
<hlm-tabs-list>
<button hlmTabsTrigger="preview">
<ng-icon name="lucideAppWindow" />
Preview
</button>
<button hlmTabsTrigger="code">
<ng-icon name="lucideCode" />
Code
</button>
</hlm-tabs-list>
</hlm-tabs>
`,
})
export class TabsWithIconsPreview {}Icons Only
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideHome, lucideSearch, lucideSettings } from '@ng-icons/lucide';
import { HlmTabsImports } from '@spartan-ng/helm/tabs';
@Component({
selector: 'spartan-tabs-icons-only',
imports: [HlmTabsImports, NgIcon],
providers: [provideIcons({ lucideHome, lucideSearch, lucideSettings })],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<hlm-tabs tab="home">
<hlm-tabs-list>
<button hlmTabsTrigger="home">
<ng-icon name="lucideHome" />
</button>
<button hlmTabsTrigger="search">
<ng-icon name="lucideSearch" />
</button>
<button hlmTabsTrigger="settings">
<ng-icon name="lucideSettings" />
</button>
</hlm-tabs-list>
</hlm-tabs>
`,
})
export class TabsIconsOnlyPreview {}With Input and Button
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { HlmTabsImports } from '@spartan-ng/helm/tabs';
@Component({
selector: 'spartan-tabs-input-button',
imports: [HlmTabsImports, HlmInputImports, HlmButtonImports],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<hlm-tabs tab="overview">
<div class="flex items-center gap-4">
<hlm-tabs-list>
<button hlmTabsTrigger="overview">Overview</button>
<button hlmTabsTrigger="analytics">Settings</button>
</hlm-tabs-list>
<div class="ml-auto flex items-center gap-2">
<input hlmInput placeholder="Search..." />
<button hlmBtn>Action</button>
</div>
</div>
<div hlmTabsContent="overview" class="rounded-lg border p-6">
View your dashboard metrics and key performance indicators.
</div>
<div hlmTabsContent="analytics" class="rounded-lg border p-6">
Detailed analytics and insights about your data.
</div>
</hlm-tabs>
`,
})
export class TabsInputButtonPreview {}Lazy Loading
Use hlmTabsContentLazy on an ng-template inside a tab panel to keep its content out of the DOM until the user navigates to that tab. This is particularly useful when panels trigger network requests or render expensive component trees. The content is created once on first visit and remains alive for subsequent visits.
Account
This content was lazily loaded when the tab was first activated.
Account content loaded lazily.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { HlmCardImports } from '@spartan-ng/helm/card';
import { HlmTabsImports } from '@spartan-ng/helm/tabs';
@Component({
selector: 'spartan-tabs-lazy',
imports: [HlmTabsImports, HlmCardImports],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class: 'block w-full max-w-lg',
},
template: `
<hlm-tabs tab="account" class="w-full">
<hlm-tabs-list>
<button hlmTabsTrigger="account">Account</button>
<button hlmTabsTrigger="password">Password</button>
</hlm-tabs-list>
<div hlmTabsContent="account">
<ng-template hlmTabsContentLazy>
<section hlmCard>
<div hlmCardHeader>
<h3 hlmCardTitle>Account</h3>
<p hlmCardDescription>This content was lazily loaded when the tab was first activated.</p>
</div>
<p hlmCardContent>Account content loaded lazily.</p>
</section>
</ng-template>
</div>
<div hlmTabsContent="password">
<ng-template hlmTabsContentLazy>
<section hlmCard>
<div hlmCardHeader>
<h3 hlmCardTitle>Password</h3>
<p hlmCardDescription>This content was lazily loaded when the tab was first activated.</p>
</div>
<p hlmCardContent>Password content loaded lazily.</p>
</section>
</ng-template>
</div>
</hlm-tabs>
`,
})
export class TabsLazyPreview {}Paginated Tabs
Use hlm-paginated-tabs-list instead of hlm-tabs-list for paginated tabs list with next and previous buttons.
Disable pagination with [disablePagination]="true" . Hides the pagination buttons and active tab is not scrolled into view.
Padding styles, applied to the tab list ( listVariants ), are not taken into account during keyboard scrolling . This affects the active tab's scrolling position and next/previous button remain enabled even when the active tab is at the start or end of the tab list.
import { Component, input } from '@angular/core';
import { HlmTabsImports } from '@spartan-ng/helm/tabs';
@Component({
selector: 'spartan-tabs-paginated',
imports: [HlmTabsImports],
host: {
class: 'block w-full max-w-lg min-h-[150px]',
},
template: `
<hlm-tabs [tab]="activeTab()" class="w-full">
<hlm-paginated-tabs-list>
@for (tab of lotsOfTabs; track tab) {
<button [hlmTabsTrigger]="tab">{{ tab }}</button>
}
</hlm-paginated-tabs-list>
@for (tab of lotsOfTabs; track tab) {
<div [hlmTabsContent]="tab">{{ tab }}</div>
}
</hlm-tabs>
`,
})
export class TabsPaginatedPreview {
public readonly activeTab = input('Tab 0');
public readonly lotsOfTabs = Array.from({ length: 30 })
.fill(0)
.map((_, index) => `Tab ${index}`);
}Brain API
BrnTabsContentLazy
Selector: ng-template[brnTabsContentLazy]
ExportAs: brnTabsContentLazy
BrnTabsContent
Selector: [brnTabsContent]
ExportAs: brnTabsContent
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| contentFor* (required) | string | - | - |
BrnTabsList
Selector: [brnTabsList]
ExportAs: brnTabsList
BrnTabsPaginatedList
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| disablePagination | boolean | false | Whether pagination should be disabled. This can be used to avoid unnecessary layout recalculations if it's known that pagination won't be required. |
Outputs
| Prop | Type | Default | Description |
|---|---|---|---|
| selectFocusedIndex | number | - | Event emitted when the option is selected. |
| indexFocused | number | - | Event emitted when a label is focused. |
BrnTabsTrigger
Selector: button[brnTabsTrigger]
ExportAs: brnTabsTrigger
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| triggerFor* (required) | string | - | - |
| disabled | boolean | false | - |
BrnTabs
Selector: [brnTabs]
ExportAs: brnTabs
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| orientation | BrnTabsOrientation | horizontal | - |
| activationMode | BrnActivationMode | automatic | - |
| activeTab | string | undefined | undefined | - |
Outputs
| Prop | Type | Default | Description |
|---|---|---|---|
| tabActivated | string | - | - |
| activeTabChange | string | undefined | undefined | - |
Helm API
HlmTabsContentLazy
Selector: ng-template[hlmTabsContentLazy]
HlmTabsContent
Selector: [hlmTabsContent]
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| contentFor* (required) | string | - | - |
HlmTabsList
Selector: [hlmTabsList],hlm-tabs-list
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | ListVariants['variant'] | default | - |
HlmTabsPaginatedList
Selector: hlm-paginated-tabs-list
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| tabListClass | ClassValue | - | - |
| paginationButtonClass | ClassValue | - | - |
HlmTabsTrigger
Selector: [hlmTabsTrigger]
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| triggerFor* (required) | string | - | - |
HlmTabs
Selector: [hlmTabs],hlm-tabs
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| tab* (required) | string | - | - |
On This Page