- 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
- 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
Button Group
A container that groups related buttons together with consistent styling.
import { Component, signal } from '@angular/core';
import { provideIcons } from '@ng-icons/core';
import {
lucideArchive,
lucideArrowLeft,
lucideCalendarPlus,
lucideChevronRight,
lucideClock,
lucideEllipsis,
lucideListFilterPlus,
lucideMailCheck,
lucideTag,
lucideTrash,
} from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';
import { HlmDropdownMenuImports } from '@spartan-ng/helm/dropdown-menu';
import { HlmIconImports } from '@spartan-ng/helm/icon';
@Component({
selector: 'spartan-button-group-preview',
imports: [HlmIconImports, HlmButtonImports, HlmButtonGroupImports, HlmDropdownMenuImports],
providers: [
provideIcons({
lucideArrowLeft,
lucideEllipsis,
lucideMailCheck,
lucideArchive,
lucideClock,
lucideCalendarPlus,
lucideListFilterPlus,
lucideTag,
lucideTrash,
lucideChevronRight,
}),
],
template: `
<div>
<div hlmButtonGroup>
<div hlmButtonGroup class="hidden sm:flex">
<button hlmBtn variant="outline" size="icon" aria-label="Go Back">
<ng-icon name="lucideArrowLeft" />
</button>
</div>
<div hlmButtonGroup>
<button hlmBtn variant="outline">Archive</button>
<button hlmBtn variant="outline">Report</button>
</div>
<div hlmButtonGroup>
<button hlmBtn variant="outline">Snooze</button>
<button hlmBtn variant="outline" aria-label="More Options" [hlmDropdownMenuTrigger]="menu" align="end">
<ng-icon name="lucideEllipsis" />
</button>
</div>
</div>
<ng-template #menu>
<hlm-dropdown-menu class="w-52">
<hlm-dropdown-menu-group>
<button hlmDropdownMenuItem>
<ng-icon hlm name="lucideMailCheck" size="sm" />
<span>Mark as Read</span>
</button>
<button hlmDropdownMenuItem>
<ng-icon hlm name="lucideArchive" size="sm" />
<span>Archive</span>
</button>
</hlm-dropdown-menu-group>
<hlm-dropdown-menu-separator />
<hlm-dropdown-menu-group>
<button hlmDropdownMenuItem>
<ng-icon hlm name="lucideClock" size="sm" />
<span>Snooze</span>
</button>
<button hlmDropdownMenuItem>
<ng-icon hlm name="lucideCalendarPlus" size="sm" />
<span>Add to Calendar</span>
</button>
<button hlmDropdownMenuItem>
<ng-icon hlm name="lucideCalendarPlus" size="sm" />
<span>Add to List</span>
</button>
<button
hlmDropdownMenuItem
class="flex justify-between"
align="start"
side="right"
[hlmDropdownMenuTrigger]="submenu"
>
<div class="flex items-center gap-2">
<ng-icon hlm name="lucideTag" size="sm" />
<span>Label as...</span>
</div>
<ng-icon hlm name="lucideChevronRight" size="sm" />
</button>
</hlm-dropdown-menu-group>
<hlm-dropdown-menu-separator />
<hlm-dropdown-menu-group>
<button
hlmDropdownMenuItem
variant="destructive"
class="hover:bg-destructive/10 dark:hover:bg-destructive/40"
>
<ng-icon hlm name="lucideTrash" size="sm" />
<span>Delete</span>
</button>
</hlm-dropdown-menu-group>
</hlm-dropdown-menu>
</ng-template>
<ng-template #submenu>
<hlm-dropdown-menu-sub class="w-40">
<button hlmDropdownMenuRadio [checked]="label() === 'personal'" (click)="label.set('personal')">
<hlm-dropdown-menu-radio-indicator />
<span>Personal</span>
</button>
<button hlmDropdownMenuRadio [checked]="label() === 'work'" (click)="label.set('work')">
<hlm-dropdown-menu-radio-indicator />
<span>Work</span>
</button>
<button hlmDropdownMenuRadio [checked]="label() === 'other'" (click)="label.set('other')">
<hlm-dropdown-menu-radio-indicator />
<span>Other</span>
</button>
</hlm-dropdown-menu-sub>
</ng-template>
</div>
`,
})
export class ButtonGroupPreview {
public readonly label = signal('personal');
}
export const accessibilityCode = `
<div hlmButtonGroup aria-label="Button group">
<button hlmBtn variant="outline">Button 1</button>
<button hlmBtn variant="outline">Button 2</button>
</div>
`;Installation
ng g @spartan-ng/cli:ui button-groupnx g @spartan-ng/cli:ui button-groupimport { 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 { BrnSeparator, provideBrnSeparatorConfig } from '@spartan-ng/brain/separator';
import { Directive, input } from '@angular/core';
import { classes } from '@spartan-ng/helm/utils';
import { cva } from 'class-variance-authority';
@Directive({
selector: '[hlmButtonGroupSeparator],hlm-button-group-separator',
providers: [provideBrnSeparatorConfig({ orientation: 'vertical' })],
hostDirectives: [{ directive: BrnSeparator, inputs: ['orientation', 'decorative'] }],
host: {
'data-slot': 'button-group-separator',
},
})
export class HlmButtonGroupSeparator {
constructor() {
classes(
() =>
'bg-input relative inline-flex shrink-0 self-stretch data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-auto data-[orientation=vertical]:w-px',
);
}
}
@Directive({
selector: '[hlmButtonGroupText],hlm-button-group-text',
})
export class HlmButtonGroupText {
constructor() {
classes(
() =>
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_ng-icon]:pointer-events-none [&_ng-icon:not([class*='text-'])]:text-base",
);
}
}
export const buttonGroupVariants = cva(
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
{
variants: {
orientation: {
horizontal:
'[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
vertical:
'flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
},
},
defaultVariants: {
orientation: 'horizontal',
},
},
);
@Directive({
selector: '[hlmButtonGroup],hlm-button-group',
host: {
'data-slot': 'button-group',
role: 'group',
'[attr.data-orientation]': 'orientation()',
},
})
export class HlmButtonGroup {
constructor() {
classes(() => buttonGroupVariants({ orientation: this.orientation() }));
}
public readonly orientation = input<'horizontal' | 'vertical'>('horizontal');
}
export const HlmButtonGroupImports = [HlmButtonGroup, HlmButtonGroupText, HlmButtonGroupSeparator] as const;Usage
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';<div hlmButtonGroup>
<button hlmBtn variant="outline">Button 1</button>
<button hlmBtn variant="outline">Button 2</button>
</div>Accessibility
- The
HlmButtonGroupdirective has theroleattribute set togroup. - Use
Tabto navigate between the buttons in the group. - Use
aria-labeloraria-labelledbyto label the button group.
<div hlmButtonGroup aria-label="Button group">
<button hlmBtn variant="outline">Button 1</button>
<button hlmBtn variant="outline">Button 2</button>
</div>ButtonGroup vs ToggleGroup
- Use the
ButtonGroupcomponent when you want to group buttons that perform an action. - Use the
ToggleGroupcomponent when you want to group buttons that toggle a state.
Examples
Orientation
Set the orientation prop to change the button group layout.
import { Component } from '@angular/core';
import { provideIcons } from '@ng-icons/core';
import { lucideMinus, lucidePlus } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';
import { HlmIconImports } from '@spartan-ng/helm/icon';
@Component({
selector: 'spartan-button-group-orientation',
imports: [HlmIconImports, HlmButtonImports, HlmButtonGroupImports],
providers: [provideIcons({ lucidePlus, lucideMinus })],
template: `
<div hlmButtonGroup orientation="vertical" aria-label="Media controls" class="h-fit">
<button hlmBtn variant="outline" size="icon">
<ng-icon hlm name="lucidePlus" size="sm" />
</button>
<button hlmBtn variant="outline" size="icon">
<ng-icon hlm name="lucideMinus" size="sm" />
</button>
</div>
`,
})
export class ButtonGroupOrientation {}Size
Control the size of buttons using the size prop on individual buttons.
import { Component } from '@angular/core';
import { provideIcons } from '@ng-icons/core';
import { lucidePlus } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';
import { HlmIconImports } from '@spartan-ng/helm/icon';
@Component({
selector: 'spartan-button-group-size',
imports: [HlmIconImports, HlmButtonImports, HlmButtonGroupImports],
providers: [provideIcons({ lucidePlus })],
template: `
<div class="flex flex-col items-start gap-8">
<div hlmButtonGroup>
<button hlmBtn variant="outline" size="sm">Small</button>
<button hlmBtn variant="outline" size="sm">Button</button>
<button hlmBtn variant="outline" size="sm">Group</button>
<button hlmBtn variant="outline" size="icon-sm">
<ng-icon hlm name="lucidePlus" size="sm" />
</button>
</div>
<div hlmButtonGroup>
<button hlmBtn variant="outline">Default</button>
<button hlmBtn variant="outline">Button</button>
<button hlmBtn variant="outline">Group</button>
<button hlmBtn variant="outline" size="icon">
<ng-icon hlm name="lucidePlus" size="sm" />
</button>
</div>
<div hlmButtonGroup>
<button hlmBtn variant="outline" size="lg">Large</button>
<button hlmBtn variant="outline" size="lg">Button</button>
<button hlmBtn variant="outline" size="lg">Group</button>
<button hlmBtn variant="outline" size="icon-lg">
<ng-icon hlm name="lucidePlus" size="sm" />
</button>
</div>
</div>
`,
})
export class ButtonGroupSize {}Nested
Nest HlmButtonGroup to create button groups with spacing.
import { Component } from '@angular/core';
import { provideIcons } from '@ng-icons/core';
import { lucideArrowLeft, lucideArrowRight } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';
import { HlmIconImports } from '@spartan-ng/helm/icon';
@Component({
selector: 'spartan-button-group-nested',
imports: [HlmIconImports, HlmButtonImports, HlmButtonGroupImports],
providers: [provideIcons({ lucideArrowRight, lucideArrowLeft })],
template: `
<div hlmButtonGroup>
<div hlmButtonGroup>
<button hlmBtn variant="outline" size="sm">1</button>
<button hlmBtn variant="outline" size="sm">2</button>
<button hlmBtn variant="outline" size="sm">3</button>
<button hlmBtn variant="outline" size="sm">4</button>
<button hlmBtn variant="outline" size="sm">5</button>
</div>
<div hlmButtonGroup>
<button hlmBtn variant="outline" size="icon-sm" aria-label="Previous">
<ng-icon hlm name="lucideArrowLeft" size="sm" />
</button>
<button hlmBtn variant="outline" size="icon-sm" aria-label="Next">
<ng-icon hlm name="lucideArrowRight" size="sm" />
</button>
</div>
</div>
`,
})
export class ButtonGroupNested {}Separator
The HlmButtonGroupSeparator component visually divides buttons within a group.
Buttons with variant outline do not need a separator since they have a border. For other variants, a separator is recommended to improve the visual hierarchy.
import { Component } from '@angular/core';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';
@Component({
selector: 'spartan-button-group-separator',
imports: [HlmButtonImports, HlmButtonGroupImports],
template: `
<div hlmButtonGroup>
<button hlmBtn variant="secondary" size="sm">Copy</button>
<hlm-button-group-separator />
<button hlmBtn variant="secondary" size="sm">Paste</button>
</div>
`,
})
export class ButtonGroupSeparator {}Split
Create a split button group by adding two buttons separated by a HlmButtonGroupSeparator .
import { Component } from '@angular/core';
import { provideIcons } from '@ng-icons/core';
import { lucidePlus } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';
import { HlmIconImports } from '@spartan-ng/helm/icon';
@Component({
selector: 'spartan-button-group-split',
imports: [HlmIconImports, HlmButtonImports, HlmButtonGroupImports],
providers: [provideIcons({ lucidePlus })],
template: `
<div hlmButtonGroup>
<button hlmBtn variant="secondary">Button</button>
<hlm-button-group-separator />
<button hlmBtn variant="secondary" size="icon">
<ng-icon hlm name="lucidePlus" size="sm" />
</button>
</div>
`,
})
export class ButtonGroupSplit {}With text
Add text to the button group using the HlmButtonGroupText component.
import { Component } from '@angular/core';
import { provideIcons } from '@ng-icons/core';
import { lucideCopy } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';
import { HlmIconImports } from '@spartan-ng/helm/icon';
import { HlmInputImports } from '@spartan-ng/helm/input';
@Component({
selector: 'spartan-button-group-with-text',
imports: [HlmIconImports, HlmButtonImports, HlmInputImports, HlmButtonGroupImports],
providers: [provideIcons({ lucideCopy })],
template: `
<div hlmButtonGroup>
<span hlmButtonGroupText>https://</span>
<input hlmInput placeholder="Website url" class="z-10" />
<button hlmBtn variant="outline" size="icon">
<ng-icon hlm name="lucideCopy" size="sm" />
</button>
</div>
`,
})
export class ButtonGroupWithText {}Input
Wrap an HlmInput component with buttons.
import { Component } from '@angular/core';
import { provideIcons } from '@ng-icons/core';
import { lucideSearch } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';
import { HlmIconImports } from '@spartan-ng/helm/icon';
import { HlmInputImports } from '@spartan-ng/helm/input';
@Component({
selector: 'spartan-button-group-input',
imports: [HlmIconImports, HlmButtonImports, HlmButtonGroupImports, HlmInputImports],
providers: [provideIcons({ lucideSearch })],
template: `
<div hlmButtonGroup>
<input hlmInput placeholder="Search..." />
<button hlmBtn variant="outline">
<ng-icon hlm name="lucideSearch" size="sm" />
</button>
</div>
`,
})
export class ButtonGroupInput {}Input Group
Wrap an HlmInputGroup component to create complex input layouts
import { Component, signal } from '@angular/core';
import { provideIcons } from '@ng-icons/core';
import { lucideAudioLines, lucidePlus } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';
import { HlmIconImports } from '@spartan-ng/helm/icon';
import { HlmInputGroupImports } from '@spartan-ng/helm/input-group';
import { HlmTooltipImports } from '@spartan-ng/helm/tooltip';
@Component({
selector: 'spartan-button-group-input-group',
imports: [HlmIconImports, HlmButtonImports, HlmButtonGroupImports, HlmInputGroupImports, HlmTooltipImports],
providers: [provideIcons({ lucidePlus, lucideAudioLines })],
template: `
<div hlmButtonGroup class="[--radius:9999rem]">
<div hlmButtonGroup>
<button hlmBtn variant="outline" size="icon">
<ng-icon hlm name="lucidePlus" size="sm" />
</button>
</div>
<div hlmButtonGroup>
<div hlmInputGroup>
<input
hlmInputGroupInput
[placeholder]="voiceEnabled() ? 'Record and send audio...' : 'Send a message...'"
[disabled]="voiceEnabled()"
/>
<div hlmInputGroupAddon align="inline-end">
<button
hlmInputGroupButton
[hlmTooltip]="'Voice Mode'"
size="icon-xs"
[attr.data-active]="voiceEnabled()"
[attr.aria-pressed]="voiceEnabled()"
class="data-[active=true]:bg-orange-100 data-[active=true]:text-orange-700 dark:data-[active=true]:bg-orange-800 dark:data-[active=true]:text-orange-100"
(click)="voiceEnabled.set(!voiceEnabled())"
>
<ng-icon hlm name="lucideAudioLines" size="sm" />
</button>
</div>
</div>
</div>
</div>
`,
})
export class ButtonGroupInputGroup {
public readonly voiceEnabled = signal(false);
}Dropdown Menu
Create a split button group with a HlmDropdownMenu component .
import { Component } from '@angular/core';
import { provideIcons } from '@ng-icons/core';
import {
lucideCheck,
lucideChevronDown,
lucideCopy,
lucideShare,
lucideTrash,
lucideTriangleAlert,
lucideUserRoundX,
lucideVolumeOff,
} from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';
import { HlmDropdownMenuImports } from '@spartan-ng/helm/dropdown-menu';
import { HlmIconImports } from '@spartan-ng/helm/icon';
@Component({
selector: 'spartan-button-group-dropdown-menu',
imports: [HlmIconImports, HlmButtonImports, HlmButtonGroupImports, HlmDropdownMenuImports],
providers: [
provideIcons({
lucideChevronDown,
lucideVolumeOff,
lucideCheck,
lucideTriangleAlert,
lucideUserRoundX,
lucideShare,
lucideCopy,
lucideTrash,
}),
],
template: `
<div hlmButtonGroup>
<button hlmBtn variant="outline">Follow</button>
<button hlmBtn variant="outline" size="icon" [hlmDropdownMenuTrigger]="menu" align="end">
<ng-icon hlm name="lucideChevronDown" size="sm" />
</button>
</div>
<ng-template #menu>
<hlm-dropdown-menu class="w-[49] [--radius:1rem]">
<hlm-dropdown-menu-group>
<button hlmDropdownMenuItem>
<ng-icon hlm name="lucideVolumeOff" size="sm" />
<span>Muted Conversation</span>
</button>
<button hlmDropdownMenuItem>
<ng-icon hlm name="lucideCheck" size="sm" />
<span>Mark as Read</span>
</button>
<button hlmDropdownMenuItem>
<ng-icon hlm name="lucideTriangleAlert" size="sm" />
<span>Report Conversation</span>
</button>
<button hlmDropdownMenuItem>
<ng-icon hlm name="lucideUserRoundX" size="sm" />
<span>Block User</span>
</button>
<button hlmDropdownMenuItem>
<ng-icon hlm name="lucideShare" size="sm" />
<span>Share Conversation</span>
</button>
<button hlmDropdownMenuItem>
<ng-icon hlm name="lucideCopy" size="sm" />
<span>Copy Conversation</span>
</button>
</hlm-dropdown-menu-group>
<hlm-dropdown-menu-separator />
<hlm-dropdown-menu-group>
<button
hlmDropdownMenuItem
variant="destructive"
class="hover:bg-destructive/10 dark:hover:bg-destructive/40"
>
<ng-icon hlm name="lucideTrash" size="sm" class="!text-destructive" />
<span>Delete Conversation</span>
</button>
</hlm-dropdown-menu-group>
</hlm-dropdown-menu>
</ng-template>
`,
})
export class ButtonGroupDropdownMenu {}Select
Pair with a Select component.
import { Component } from '@angular/core';
import { provideIcons } from '@ng-icons/core';
import { lucideArrowRight } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';
import { HlmIconImports } from '@spartan-ng/helm/icon';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { HlmSelectImports } from '@spartan-ng/helm/select';
@Component({
selector: 'spartan-button-group-select',
imports: [HlmIconImports, HlmInputImports, HlmButtonImports, HlmSelectImports, HlmButtonGroupImports],
providers: [provideIcons({ lucideArrowRight })],
template: `
<div hlmButtonGroup>
<div hlmButtonGroup class="[&>hlm-select>hlm-select-trigger>button]:rounded-r-none">
<hlm-select class="inline-block" [value]="_currencies[0].value">
<hlm-select-trigger>
<hlm-select-value placeholder="Select an option" />
</hlm-select-trigger>
<hlm-select-content *hlmSelectPortal class="!min-w-40">
<hlm-select-group>
@for (currency of _currencies; track currency.label) {
<hlm-select-item [value]="currency.value">
<span>
{{ currency.value }}
</span>
<span class="text-muted-foreground">{{ currency.label }}</span>
</hlm-select-item>
}
</hlm-select-group>
</hlm-select-content>
</hlm-select>
<input hlmInput placeholder="10.00" />
</div>
<div hlmButtonGroup>
<button hlmBtn variant="outline" size="icon">
<ng-icon hlm name="lucideArrowRight" size="sm" />
</button>
</div>
</div>
`,
})
export class ButtonGroupSelect {
protected readonly _currencies = [
{
value: '$',
label: 'US Dollar',
},
{
value: '€',
label: 'Euro',
},
{
value: '£',
label: 'British Pound',
},
];
}Popover
Use with a Popover component.
import { Component } from '@angular/core';
import { provideIcons } from '@ng-icons/core';
import { lucideBot, lucideChevronDown } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmButtonGroupImports } from '@spartan-ng/helm/button-group';
import { HlmIconImports } from '@spartan-ng/helm/icon';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { HlmPopoverImports } from '@spartan-ng/helm/popover';
@Component({
selector: 'spartan-button-group-popover',
imports: [HlmIconImports, HlmInputImports, HlmButtonImports, HlmButtonGroupImports, HlmPopoverImports],
providers: [provideIcons({ lucideChevronDown, lucideBot })],
template: `
<hlm-popover sideOffset="5" align="end">
<div hlmButtonGroup>
<button hlmBtn variant="outline">
<ng-icon hlm name="lucideBot" size="sm" />
Copilot
</button>
<button id="edit-profile" variant="outline" hlmPopoverTrigger hlmBtn variant="outline" size="icon">
<ng-icon hlm name="lucideChevronDown" size="sm" />
</button>
<hlm-popover-content class="rounded-xl p-0 text-sm" *hlmPopoverPortal="let ctx">
<div class="border-input border-b px-4 py-3">
<div class="text-sm font-medium">Agent Tasks</div>
</div>
<div class="p-4 text-sm">
<textarea
hlmInput
placeholder="Describe your task in natural language."
class="mb-4 min-h-16 resize-none"
></textarea>
<p class="mb-2 font-medium">Start a new task with Copilot</p>
<p class="text-muted-foreground">
Describe your task in natural language. Copilot will work in the background and open a pull request for
your review.
</p>
</div>
</hlm-popover-content>
</div>
</hlm-popover>
`,
})
export class ButtonGroupPopover {}Helm API
HlmButtonGroupSeparator
Selector: [hlmButtonGroupSeparator],hlm-button-group-separator
HlmButtonGroupText
Selector: [hlmButtonGroupText],hlm-button-group-text
HlmButtonGroup
Selector: [hlmButtonGroup],hlm-button-group
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| orientation | 'horizontal' | 'vertical' | horizontal | - |
On This Page