Button
Triggers an action with a side effect. Pure navigation is a link (`<a>`, router link) — even when it should look like a button.
Added 14 October 2026
Playground
React
Codecomponents/react/button-demo.tsx
import {Button} from '@stroeer-labs/design-library/react';
import {PlusIcon} from 'lucide-react';
import {type ComponentProps, useEffect, useState} from 'react';
import {buttonIconSizeOptions, buttonPlayground, buttonTextSizeOptions, buttonVariantOptions, isIconSize} from '../playground/button-playground';
import {playgroundChangeEvent, type PlaygroundSelection, readSelection} from '../playground/playground';
type ButtonVariant = ComponentProps<typeof Button>['variant'];
type ButtonSize = ComponentProps<typeof Button>['size'];
function usePlaygroundSelection(): PlaygroundSelection | null {
const [selection, setSelection] = useState<PlaygroundSelection | null>(() => readSelection(buttonPlayground));
useEffect(() => {
const onChange = () => setSelection(readSelection(buttonPlayground));
window.addEventListener(playgroundChangeEvent, onChange);
return () => window.removeEventListener(playgroundChangeEvent, onChange);
}, []);
return selection;
}
export function ButtonDemo() {
const selection = usePlaygroundSelection();
if (selection) {
return (
<div className="flex min-h-32 items-center justify-center">
<Button
variant={selection.variant as ButtonVariant}
size={selection.size as ButtonSize}
disabled={selection.disabled === 'true'}
aria-label={isIconSize(selection) ? 'Add' : undefined}
>
{isIconSize(selection) ? <PlusIcon /> : 'Button'}
</Button>
</div>
);
}
return (
<div className="space-y-6">
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Variants</p>
<div className="flex flex-wrap gap-3">
{buttonVariantOptions.map((option) => (
<Button key={option.value} variant={option.value as ButtonVariant}>
{option.label}
</Button>
))}
</div>
</div>
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Sizes</p>
<div className="flex flex-wrap items-center gap-3">
{buttonTextSizeOptions.map((option) => (
<Button key={option.value} size={option.value as ButtonSize}>
{option.label}
</Button>
))}
</div>
</div>
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Icon sizes</p>
<div className="flex flex-wrap items-center gap-3">
{buttonIconSizeOptions.map((option) => (
<Button key={option.value} size={option.value as ButtonSize} aria-label="Add">
<PlusIcon />
</Button>
))}
</div>
</div>
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Disabled</p>
<div className="flex flex-wrap gap-3">
<Button disabled>Default</Button>
<Button variant="secondary" disabled>
Secondary
</Button>
<Button variant="destructive" disabled>
Destructive
</Button>
</div>
</div>
</div>
);
}Angular
Codecomponents/angular/button-demo.ts
import {Component, type OnDestroy, signal} from '@angular/core';
import {NgIcon, provideIcons} from '@ng-icons/core';
import {lucidePlus} from '@ng-icons/lucide';
import {HlmButtonImports} from '@stroeer-labs/design-library/angular';
import {buttonIconSizeOptions, buttonPlayground, buttonTextSizeOptions, buttonVariantOptions, isIconSize} from '../playground/button-playground';
import {type PlaygroundSelection, playgroundChangeEvent, readSelection} from '../playground/playground';
@Component({
selector: 'dl-button-demo',
standalone: true,
imports: [HlmButtonImports, NgIcon],
viewProviders: [provideIcons({lucidePlus})],
template: `
@if (selection(); as sel) {
<div class="flex min-h-32 items-center justify-center">
@if (isIcon(sel)) {
<button hlmBtn [variant]="$any(sel['variant'])" [size]="$any(sel['size'])" [disabled]="sel['disabled'] === 'true'" aria-label="Add">
<ng-icon name="lucidePlus" />
</button>
} @else {
<button hlmBtn [variant]="$any(sel['variant'])" [size]="$any(sel['size'])" [disabled]="sel['disabled'] === 'true'">Button</button>
}
</div>
} @else {
<div class="space-y-6">
<div class="space-y-2">
<p class="text-xs font-medium text-muted-foreground">Variants</p>
<div class="flex flex-wrap gap-3">
@for (option of variants; track option.value) {
<button hlmBtn [variant]="$any(option.value)">{{ option.label }}</button>
}
</div>
</div>
<div class="space-y-2">
<p class="text-xs font-medium text-muted-foreground">Sizes</p>
<div class="flex flex-wrap items-center gap-3">
@for (option of textSizes; track option.value) {
<button hlmBtn [size]="$any(option.value)">{{ option.label }}</button>
}
</div>
</div>
<div class="space-y-2">
<p class="text-xs font-medium text-muted-foreground">Icon sizes</p>
<div class="flex flex-wrap items-center gap-3">
@for (option of iconSizes; track option.value) {
<button hlmBtn [size]="$any(option.value)" aria-label="Add"><ng-icon name="lucidePlus" /></button>
}
</div>
</div>
<div class="space-y-2">
<p class="text-xs font-medium text-muted-foreground">Disabled</p>
<div class="flex flex-wrap gap-3">
<button hlmBtn [disabled]="true">Default</button>
<button hlmBtn variant="secondary" [disabled]="true">Secondary</button>
<button hlmBtn variant="destructive" [disabled]="true">Destructive</button>
</div>
</div>
</div>
}
`,
})
export class ButtonDemoComponent implements OnDestroy {
protected readonly variants = buttonVariantOptions;
protected readonly textSizes = buttonTextSizeOptions;
protected readonly iconSizes = buttonIconSizeOptions;
protected readonly selection = signal<PlaygroundSelection | null>(readSelection(buttonPlayground));
private readonly onPlaygroundChange = () => this.selection.set(readSelection(buttonPlayground));
constructor() {
window.addEventListener(playgroundChangeEvent, this.onPlaygroundChange);
}
ngOnDestroy() {
window.removeEventListener(playgroundChangeEvent, this.onPlaygroundChange);
}
protected isIcon(selection: PlaygroundSelection): boolean {
return isIconSize(selection);
}
}