Skip to content
Navigation

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);
    }
}