- 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
Alert
Displays a callout for user attention.
Payment successful
Your payment of $29.99 has been processed. A receipt has been sent to your email address.
New feature available
We've added dark mode support. You can enable it in your account settings.
import { Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideCircleCheck, lucideInfo } from '@ng-icons/lucide';
import { HlmAlertImports } from '@spartan-ng/helm/alert';
@Component({
selector: 'spartan-alert-preview',
imports: [HlmAlertImports, NgIcon],
providers: [provideIcons({ lucideCircleCheck, lucideInfo })],
template: `
<div class="grid w-full max-w-md items-start gap-4">
<hlm-alert>
<ng-icon name="lucideCircleCheck" />
<h4 hlmAlertTitle>Payment successful</h4>
<p hlmAlertDescription>
Your payment of $29.99 has been processed. A receipt has been sent to your email address.
</p>
</hlm-alert>
<hlm-alert>
<ng-icon name="lucideInfo" />
<h4 hlmAlertTitle>New feature available</h4>
<p hlmAlertDescription>We've added dark mode support. You can enable it in your account settings.</p>
</hlm-alert>
</div>
`,
})
export class AlertPreview {}Installation
ng g @spartan-ng/cli:ui alertnx g @spartan-ng/cli:ui alertimport { 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 { Directive, input } from '@angular/core';
import { classes } from '@spartan-ng/helm/utils';
import { cva, type VariantProps } from 'class-variance-authority';
@Directive({
selector: '[hlmAlertAction]',
host: {
'data-slot': 'alert-action',
},
})
export class HlmAlertAction {
constructor() {
classes(() => 'absolute end-3 top-2.5');
}
}
@Directive({
selector: '[hlmAlertDescription]',
host: {
'data-slot': 'alert-description',
},
})
export class HlmAlertDescription {
constructor() {
classes(() => 'text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3');
}
}
@Directive({
selector: '[hlmAlertTitle]',
host: {
'data-slot': 'alert-title',
},
})
export class HlmAlertTitle {
constructor() {
classes(() => 'font-medium group-has-[>ng-icon]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3');
}
}
const alertVariants = cva('grid gap-0.5 rounded-lg border px-4 py-3 text-start text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>ng-icon]:grid-cols-[auto_1fr] has-[>ng-icon]:gap-x-2.5 *:[ng-icon]:row-span-2 *:[ng-icon]:translate-y-0.5 *:[ng-icon]:text-current *:[ng-icon:not([class*=\'text-\'])]:text-[calc(var(--spacing)*4)] group/alert relative w-full', {
variants: {
variant: {
default: 'bg-card text-card-foreground',
destructive: 'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[ng-icon]:text-current',
},
},
defaultVariants: {
variant: 'default',
},
});
export type AlertVariants = VariantProps<typeof alertVariants>;
@Directive({
selector: 'hlm-alert,[hlmAlert]',
host: {
'data-slot': 'alert',
role: 'alert',
},
})
export class HlmAlert {
public readonly variant = input<AlertVariants['variant']>('default');
constructor() {
classes(() => alertVariants({ variant: this.variant() }));
}
}
export const HlmAlertImports = [HlmAlert, HlmAlertAction, HlmAlertDescription, HlmAlertTitle] as const;import { Directive, input } from '@angular/core';
import { classes } from '@spartan-ng/helm/utils';
import { cva, type VariantProps } from 'class-variance-authority';
@Directive({
selector: '[hlmAlertAction]',
host: {
'data-slot': 'alert-action',
},
})
export class HlmAlertAction {
constructor() {
classes(() => 'absolute end-[calc(--spacing(1.25))] top-[calc(--spacing(1.25))]');
}
}
@Directive({
selector: '[hlmAlertDescription]',
host: {
'data-slot': 'alert-description',
},
})
export class HlmAlertDescription {
constructor() {
classes(() => 'text-muted-foreground text-xs/relaxed text-balance md:text-pretty [&_p:not(:last-child)]:mb-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3');
}
}
@Directive({
selector: '[hlmAlertTitle]',
host: {
'data-slot': 'alert-title',
},
})
export class HlmAlertTitle {
constructor() {
classes(() => 'font-medium group-has-[>ng-icon]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3');
}
}
const alertVariants = cva('grid gap-0.5 rounded-none border px-2.5 py-2 text-start text-xs has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pe-18 has-[>ng-icon]:grid-cols-[auto_1fr] has-[>ng-icon]:gap-x-2 *:[ng-icon]:row-span-2 *:[ng-icon]:translate-y-0 *:[ng-icon]:text-current *:[ng-icon:not([class*=\'text-\'])]:text-[calc(var(--spacing)*4)] group/alert relative w-full', {
variants: {
variant: {
default: 'bg-card text-card-foreground',
destructive: 'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[ng-icon]:text-current',
},
},
defaultVariants: {
variant: 'default',
},
});
export type AlertVariants = VariantProps<typeof alertVariants>;
@Directive({
selector: 'hlm-alert,[hlmAlert]',
host: {
'data-slot': 'alert',
role: 'alert',
},
})
export class HlmAlert {
public readonly variant = input<AlertVariants['variant']>('default');
constructor() {
classes(() => alertVariants({ variant: this.variant() }));
}
}
export const HlmAlertImports = [HlmAlert, HlmAlertAction, HlmAlertDescription, HlmAlertTitle] as const;import { Directive, input } from '@angular/core';
import { classes } from '@spartan-ng/helm/utils';
import { cva, type VariantProps } from 'class-variance-authority';
@Directive({
selector: '[hlmAlertAction]',
host: {
'data-slot': 'alert-action',
},
})
export class HlmAlertAction {
constructor() {
classes(() => 'absolute end-3 top-2.5');
}
}
@Directive({
selector: '[hlmAlertDescription]',
host: {
'data-slot': 'alert-description',
},
})
export class HlmAlertDescription {
constructor() {
classes(() => 'text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3');
}
}
@Directive({
selector: '[hlmAlertTitle]',
host: {
'data-slot': 'alert-title',
},
})
export class HlmAlertTitle {
constructor() {
classes(() => 'font-medium group-has-[>ng-icon]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3');
}
}
const alertVariants = cva('grid gap-0.5 rounded-lg border px-4 py-3 text-start text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pe-18 has-[>ng-icon]:grid-cols-[auto_1fr] has-[>ng-icon]:gap-x-2.5 *:[ng-icon]:row-span-2 *:[ng-icon]:translate-y-0.5 *:[ng-icon]:text-current *:[ng-icon:not([class*=\'text-\'])]:text-[calc(var(--spacing)*4)] group/alert relative w-full', {
variants: {
variant: {
default: 'bg-card text-card-foreground',
destructive: 'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[ng-icon]:text-current',
},
},
defaultVariants: {
variant: 'default',
},
});
export type AlertVariants = VariantProps<typeof alertVariants>;
@Directive({
selector: 'hlm-alert,[hlmAlert]',
host: {
'data-slot': 'alert',
role: 'alert',
},
})
export class HlmAlert {
public readonly variant = input<AlertVariants['variant']>('default');
constructor() {
classes(() => alertVariants({ variant: this.variant() }));
}
}
export const HlmAlertImports = [HlmAlert, HlmAlertAction, HlmAlertDescription, HlmAlertTitle] as const;import { Directive, input } from '@angular/core';
import { classes } from '@spartan-ng/helm/utils';
import { cva, type VariantProps } from 'class-variance-authority';
@Directive({
selector: '[hlmAlertAction]',
host: {
'data-slot': 'alert-action',
},
})
export class HlmAlertAction {
constructor() {
classes(() => 'absolute end-2 top-1.5');
}
}
@Directive({
selector: '[hlmAlertDescription]',
host: {
'data-slot': 'alert-description',
},
})
export class HlmAlertDescription {
constructor() {
classes(() => 'text-muted-foreground text-xs/relaxed text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3');
}
}
@Directive({
selector: '[hlmAlertTitle]',
host: {
'data-slot': 'alert-title',
},
})
export class HlmAlertTitle {
constructor() {
classes(() => 'font-medium group-has-[>ng-icon]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3');
}
}
const alertVariants = cva('grid gap-0.5 rounded-lg border px-2 py-1.5 text-start text-xs/relaxed has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pe-18 has-[>ng-icon]:grid-cols-[auto_1fr] has-[>ng-icon]:gap-x-1.5 *:[ng-icon]:row-span-2 *:[ng-icon]:translate-y-0.5 *:[ng-icon]:text-current *:[ng-icon:not([class*=\'text-\'])]:text-[calc(var(--spacing)*3.5)] group/alert relative w-full', {
variants: {
variant: {
default: 'bg-card text-card-foreground',
destructive: 'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[ng-icon]:text-current',
},
},
defaultVariants: {
variant: 'default',
},
});
export type AlertVariants = VariantProps<typeof alertVariants>;
@Directive({
selector: 'hlm-alert,[hlmAlert]',
host: {
'data-slot': 'alert',
role: 'alert',
},
})
export class HlmAlert {
public readonly variant = input<AlertVariants['variant']>('default');
constructor() {
classes(() => alertVariants({ variant: this.variant() }));
}
}
export const HlmAlertImports = [HlmAlert, HlmAlertAction, HlmAlertDescription, HlmAlertTitle] as const;import { Directive, input } from '@angular/core';
import { classes } from '@spartan-ng/helm/utils';
import { cva, type VariantProps } from 'class-variance-authority';
@Directive({
selector: '[hlmAlertAction]',
host: {
'data-slot': 'alert-action',
},
})
export class HlmAlertAction {
constructor() {
classes(() => 'absolute end-2 top-2');
}
}
@Directive({
selector: '[hlmAlertDescription]',
host: {
'data-slot': 'alert-description',
},
})
export class HlmAlertDescription {
constructor() {
classes(() => 'text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3');
}
}
@Directive({
selector: '[hlmAlertTitle]',
host: {
'data-slot': 'alert-title',
},
})
export class HlmAlertTitle {
constructor() {
classes(() => 'font-medium group-has-[>ng-icon]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3');
}
}
const alertVariants = cva('grid gap-0.5 rounded-lg border px-2.5 py-2 text-start text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pe-18 has-[>ng-icon]:grid-cols-[auto_1fr] has-[>ng-icon]:gap-x-2 *:[ng-icon]:row-span-2 *:[ng-icon]:translate-y-0.5 *:[ng-icon]:text-current *:[ng-icon:not([class*=\'text-\'])]:text-[calc(var(--spacing)*4)] group/alert relative w-full', {
variants: {
variant: {
default: 'bg-card text-card-foreground',
destructive: 'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[ng-icon]:text-current',
},
},
defaultVariants: {
variant: 'default',
},
});
export type AlertVariants = VariantProps<typeof alertVariants>;
@Directive({
selector: 'hlm-alert,[hlmAlert]',
host: {
'data-slot': 'alert',
role: 'alert',
},
})
export class HlmAlert {
public readonly variant = input<AlertVariants['variant']>('default');
constructor() {
classes(() => alertVariants({ variant: this.variant() }));
}
}
export const HlmAlertImports = [HlmAlert, HlmAlertAction, HlmAlertDescription, HlmAlertTitle] as const;import { Directive, input } from '@angular/core';
import { classes } from '@spartan-ng/helm/utils';
import { cva, type VariantProps } from 'class-variance-authority';
@Directive({
selector: '[hlmAlertAction]',
host: {
'data-slot': 'alert-action',
},
})
export class HlmAlertAction {
constructor() {
classes(() => 'absolute end-3 top-2.5');
}
}
@Directive({
selector: '[hlmAlertDescription]',
host: {
'data-slot': 'alert-description',
},
})
export class HlmAlertDescription {
constructor() {
classes(() => 'text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3');
}
}
@Directive({
selector: '[hlmAlertTitle]',
host: {
'data-slot': 'alert-title',
},
})
export class HlmAlertTitle {
constructor() {
classes(() => 'font-medium group-has-[>ng-icon]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3');
}
}
const alertVariants = cva('grid gap-0.5 rounded-2xl border px-4 py-3 text-start text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pe-18 has-[>ng-icon]:grid-cols-[auto_1fr] has-[>ng-icon]:gap-x-2.5 *:[ng-icon]:row-span-2 *:[ng-icon]:translate-y-0.5 *:[ng-icon]:text-current *:[ng-icon:not([class*=\'text-\'])]:text-[calc(var(--spacing)*4)] group/alert relative w-full', {
variants: {
variant: {
default: 'bg-card text-card-foreground',
destructive: 'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[ng-icon]:text-current',
},
},
defaultVariants: {
variant: 'default',
},
});
export type AlertVariants = VariantProps<typeof alertVariants>;
@Directive({
selector: 'hlm-alert,[hlmAlert]',
host: {
'data-slot': 'alert',
role: 'alert',
},
})
export class HlmAlert {
public readonly variant = input<AlertVariants['variant']>('default');
constructor() {
classes(() => alertVariants({ variant: this.variant() }));
}
}
export const HlmAlertImports = [HlmAlert, HlmAlertAction, HlmAlertDescription, HlmAlertTitle] as const;Usage
import { HlmAlertImports } from '@spartan-ng/helm/alert';
import { NgIcon } from '@ng-icons/core';<hlm-alert variant="default | destructive">
<ng-icon name="lucideCircleCheck" />
<h4 hlmAlertTitle>Heads up!</h4>
<div hlmAlertDescription>You can add components and dependencies to your app using the cli.</div>
<div hlmAlertAction>
<button hlmBtn variant="outline" size="xs">Enable</button>
</div>
</hlm-alert>Examples
Destructive
Use variant="destructive" to create a destructive alert.
Payment failed
Your payment could not be processed. Please check your payment method and try again.
import { Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideAlertCircle } from '@ng-icons/lucide';
import { HlmAlertImports } from '@spartan-ng/helm/alert';
@Component({
selector: 'spartan-alert-destructive',
imports: [HlmAlertImports, NgIcon],
providers: [provideIcons({ lucideAlertCircle })],
template: `
<hlm-alert variant="destructive" class="max-w-md">
<ng-icon name="lucideAlertCircle" />
<h4 hlmAlertTitle>Payment failed</h4>
<p hlmAlertDescription>Your payment could not be processed. Please check your payment method and try again.</p>
</hlm-alert>
`,
})
export class AlertDestructive {}Action
Use hlmAlertAction to add a button or other action element to the alert.
Dark mode is now available
Enable it under your profile settings to get started.
import { Component } from '@angular/core';
import { HlmAlertImports } from '@spartan-ng/helm/alert';
import { HlmButtonImports } from '@spartan-ng/helm/button';
@Component({
selector: 'spartan-alert-action',
imports: [HlmAlertImports, HlmButtonImports],
template: `
<hlm-alert class="max-w-md">
<h4 hlmAlertTitle>Dark mode is now available</h4>
<p hlmAlertDescription>Enable it under your profile settings to get started.</p>
<div hlmAlertAction>
<button hlmBtn size="xs">Enable</button>
</div>
</hlm-alert>
`,
})
export class AlertAction {}Custom Colors
You can customize the alert colors by adding custom classes such as bg-amber-50 dark:bg-amber-950 to the hlm-alert component.
Your subscription will expire in 3 days.
Renew now to avoid service interruption or upgrade to a paid plan to continue using the service.
import { Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideAlertTriangle } from '@ng-icons/lucide';
import { HlmAlertImports } from '@spartan-ng/helm/alert';
@Component({
selector: 'spartan-alert-custom-colors',
imports: [HlmAlertImports, NgIcon],
providers: [provideIcons({ lucideAlertTriangle })],
template: `
<hlm-alert
class="max-w-md border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-50"
>
<ng-icon name="lucideAlertTriangle" />
<h4 hlmAlertTitle>Your subscription will expire in 3 days.</h4>
<p hlmAlertDescription>
Renew now to avoid service interruption or upgrade to a paid plan to continue using the service.
</p>
</hlm-alert>
`,
})
export class AlertCustomColors {}RTL
To enable RTL support in spartan-ng, see the RTL configuration guide.
تم الدفع بنجاح
تمت معالجة دفعتك البالغة 29.99 دولارًا. تم إرسال إيصال إلى عنوان بريدك الإلكتروني.
ميزة جديدة متاحة
لقد أضفنا دعم الوضع الداكن. يمكنك تفعيله في إعدادات حسابك.
import { Component, computed, inject } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideCheckCircle2, lucideInfo } from '@ng-icons/lucide';
import { TranslateService, Translations } from '@spartan-ng/app/app/shared/translate.service';
import { HlmAlertImports } from '@spartan-ng/helm/alert';
@Component({
selector: 'spartan-alert-rtl-preview',
imports: [NgIcon, HlmAlertImports],
providers: [
provideIcons({
lucideCheckCircle2,
lucideInfo,
}),
],
host: {
class: 'grid w-full max-w-md items-start gap-4',
'[dir]': '_dir()',
},
template: `
@for (alert of _alerts; track alert) {
<hlm-alert>
<ng-icon [name]="alert.icon" />
<h4 hlmAlertTitle>{{ _t()[alert.titleKey] }}</h4>
<p hlmAlertDescription>
{{ _t()[alert.descriptionKey] }}
</p>
</hlm-alert>
}
`,
})
export class AlertRtlPreview {
private readonly _language = inject(TranslateService).language;
private readonly _translations: Translations = {
en: {
dir: 'ltr',
values: {
paymentTitle: 'Payment successful',
paymentDescription: 'Your payment of $29.99 has been processed. A receipt has been sent to your email address.',
featureTitle: 'New feature available',
featureDescription: "We've added dark mode support. You can enable it in your account settings.",
},
},
ar: {
dir: 'rtl',
values: {
paymentTitle: 'تم الدفع بنجاح',
paymentDescription: 'تمت معالجة دفعتك البالغة 29.99 دولارًا. تم إرسال إيصال إلى عنوان بريدك الإلكتروني.',
featureTitle: 'ميزة جديدة متاحة',
featureDescription: 'لقد أضفنا دعم الوضع الداكن. يمكنك تفعيله في إعدادات حسابك.',
},
},
he: {
dir: 'rtl',
values: {
paymentTitle: 'התשלום בוצע בהצלחה',
paymentDescription: 'התשלום שלך בסך 29.99 דולר עובד. קבלה נשלחה לכתובת האימייל שלך.',
featureTitle: 'תכונה חדשה זמינה',
featureDescription: 'הוספנו תמיכה במצב כהה. אתה יכול להפעיל אותו בהגדרות החשבון שלך.',
},
},
};
protected readonly _alerts = [
{
icon: 'lucideCheckCircle2',
titleKey: 'paymentTitle' as const,
descriptionKey: 'paymentDescription' as const,
},
{
icon: 'lucideInfo',
titleKey: 'featureTitle' as const,
descriptionKey: 'featureDescription' as const,
},
];
private readonly _translation = computed(() => this._translations[this._language()]);
protected readonly _t = computed(() => this._translation().values);
protected readonly _dir = computed(() => this._translation().dir);
}Helm API
HlmAlertAction
Selector: [hlmAlertAction]
HlmAlertDescription
Selector: [hlmAlertDescription]
HlmAlertTitle
Selector: [hlmAlertTitle]
HlmAlert
Selector: hlm-alert,[hlmAlert]
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | AlertVariants['variant'] | default | - |
On This Page