Skip to content
Navigation

Menu

Added 14 October 2026

React

Codecomponents/react/menu-demo.tsx
import {useState} from 'react';
import {
    Menu,
    MenuItem,
    MenuItemDot,
    MenuLabel,
    MenuSeparator,
    ItemActions,
    ItemContent,
    ItemMedia,
    ItemTitle,
    ItemDescription,
} from '@stroeer-labs/design-library/react';
import {LayoutGridIcon, MegaphoneIcon, FileTextIcon, ArchiveIcon, ListIcon, PlusIcon} from 'lucide-react';

const nav = [
    {id: 'attention', label: 'Needs attention', icon: MegaphoneIcon, count: 2},
    {id: 'active', label: 'Active', icon: LayoutGridIcon, count: 12},
    {id: 'ended', label: 'Ended', icon: ArchiveIcon, count: 41},
];

const lineItems = [
    {id: '1', title: 'Line item 1', meta: 'Infoscreen · Praha', tone: 'success' as const},
    {id: '2', title: 'Line item 2', meta: 'Citylight · Brno', tone: 'warning' as const},
    {id: '3', title: 'Line item 3', meta: 'Pick a medium', tone: 'muted' as const},
];

export function MenuDemo() {
    const [activeNav, setActiveNav] = useState('active');
    const [activeItem, setActiveItem] = useState('1');

    return (
        <div className="grid gap-8 md:grid-cols-2">
            <div className="space-y-2">
                <p className="text-xs font-medium text-muted-foreground">Navigation menu (in a panel)</p>
                {/* The same Menu, wrapped in a card-like panel — "menu in a card". */}
                <div className="rounded-xl border border-border bg-card p-2 shadow-sm">
                    <Menu>
                        <MenuLabel>Campaigns</MenuLabel>
                        {nav.map((entry) => (
                            <MenuItem key={entry.id} active={activeNav === entry.id} onClick={() => setActiveNav(entry.id)}>
                                <ItemMedia>
                                    <entry.icon className="size-4" />
                                </ItemMedia>
                                <ItemContent>
                                    <ItemTitle>{entry.label}</ItemTitle>
                                </ItemContent>
                                <ItemActions>
                                    <span className="text-xs tabular-nums text-muted-foreground">{entry.count}</span>
                                </ItemActions>
                            </MenuItem>
                        ))}
                        <MenuSeparator />
                        <MenuItem disabled onClick={() => undefined}>
                            <ItemMedia>
                                <FileTextIcon className="size-4" />
                            </ItemMedia>
                            <ItemContent>
                                <ItemTitle>Drafts</ItemTitle>
                            </ItemContent>
                        </MenuItem>
                    </Menu>
                </div>
            </div>

            <div className="space-y-6">
                <div className="space-y-2">
                    <p className="text-xs font-medium text-muted-foreground">Line items (flat list)</p>
                    <Menu>
                        <MenuLabel>
                            Line items
                            <span className="rounded-full bg-muted px-1.5 text-[11px] tabular-nums">{lineItems.length}</span>
                        </MenuLabel>
                        {lineItems.map((item) => (
                            <MenuItem key={item.id} size="sm" current="true" active={activeItem === item.id} onClick={() => setActiveItem(item.id)}>
                                <ItemMedia>
                                    <MenuItemDot tone={item.tone} />
                                </ItemMedia>
                                <ItemContent>
                                    <ItemTitle>{item.title}</ItemTitle>
                                    <ItemDescription>{item.meta}</ItemDescription>
                                </ItemContent>
                            </MenuItem>
                        ))}
                        <MenuItem size="sm" className="border-dashed border-border" onClick={() => undefined}>
                            <ItemMedia>
                                <PlusIcon className="size-3.5" />
                            </ItemMedia>
                            <ItemContent>
                                <ItemTitle>Add line item</ItemTitle>
                            </ItemContent>
                        </MenuItem>
                    </Menu>
                </div>

                <div className="space-y-2">
                    <p className="text-xs font-medium text-muted-foreground">Card items (outline variant)</p>
                    <Menu>
                        {lineItems.slice(0, 2).map((item) => (
                            <MenuItem key={item.id} variant="outline" onClick={() => undefined}>
                                <ItemMedia>
                                    <MenuItemDot tone={item.tone} />
                                </ItemMedia>
                                <ItemContent>
                                    <ItemTitle>{item.title}</ItemTitle>
                                    <ItemDescription>{item.meta}</ItemDescription>
                                </ItemContent>
                                <ItemActions>
                                    <ListIcon className="size-4 text-muted-foreground" />
                                </ItemActions>
                            </MenuItem>
                        ))}
                    </Menu>
                </div>
            </div>
        </div>
    );
}

Angular

Codecomponents/angular/menu-demo.ts
import {Component} from '@angular/core';
import {NgIcon, provideIcons} from '@ng-icons/core';
import {lucideLayoutGrid, lucideMegaphone, lucideFileText, lucideArchive, lucideList, lucidePlus} from '@ng-icons/lucide';
import {MenuImports, HlmItemImports} from '@stroeer-labs/design-library/angular';

type Tone = 'success' | 'warning' | 'muted';

@Component({
    selector: 'dl-menu-demo',
    standalone: true,
    imports: [MenuImports, HlmItemImports, NgIcon],
    viewProviders: [provideIcons({lucideLayoutGrid, lucideMegaphone, lucideFileText, lucideArchive, lucideList, lucidePlus})],
    template: `
    <div class="grid gap-8 md:grid-cols-2">
      <div class="space-y-2">
        <p class="text-xs font-medium text-muted-foreground">Navigation menu (in a panel)</p>
        <div class="rounded-xl border border-border bg-card p-2 shadow-sm">
          <nav dlMenu>
            <div dlMenuLabel>Campaigns</div>
            @for (entry of nav; track entry.id) {
              <button dlMenuItem [active]="activeNav === entry.id" (click)="activeNav = entry.id">
                <div hlmItemMedia><ng-icon [name]="entry.icon" class="size-4" /></div>
                <div hlmItemContent><div hlmItemTitle>{{ entry.label }}</div></div>
                <div hlmItemActions>
                  <span class="text-xs tabular-nums text-muted-foreground">{{ entry.count }}</span>
                </div>
              </button>
            }
            <div dlMenuSeparator></div>
            <button dlMenuItem [disabled]="true">
              <div hlmItemMedia><ng-icon name="lucideFileText" class="size-4" /></div>
              <div hlmItemContent><div hlmItemTitle>Drafts</div></div>
            </button>
          </nav>
        </div>
      </div>

      <div class="space-y-6">
        <div class="space-y-2">
          <p class="text-xs font-medium text-muted-foreground">Line items (flat list)</p>
          <div dlMenu>
            <div dlMenuLabel>
              Line items
              <span class="rounded-full bg-muted px-1.5 text-[11px] tabular-nums">{{ lineItems.length }}</span>
            </div>
            @for (item of lineItems; track item.id) {
              <button dlMenuItem size="sm" current="true" [active]="activeItem === item.id" (click)="activeItem = item.id">
                <div hlmItemMedia><span dlMenuItemDot [tone]="item.tone"></span></div>
                <div hlmItemContent>
                  <div hlmItemTitle>{{ item.title }}</div>
                  <p hlmItemDescription>{{ item.meta }}</p>
                </div>
              </button>
            }
            <button dlMenuItem size="sm" class="border-dashed border-border">
              <div hlmItemMedia><ng-icon name="lucidePlus" class="size-3.5" /></div>
              <div hlmItemContent><div hlmItemTitle>Add line item</div></div>
            </button>
          </div>
        </div>

        <div class="space-y-2">
          <p class="text-xs font-medium text-muted-foreground">Card items (outline variant)</p>
          <div dlMenu>
            @for (item of cardItems; track item.id) {
              <button dlMenuItem variant="outline">
                <div hlmItemMedia><span dlMenuItemDot [tone]="item.tone"></span></div>
                <div hlmItemContent>
                  <div hlmItemTitle>{{ item.title }}</div>
                  <p hlmItemDescription>{{ item.meta }}</p>
                </div>
                <div hlmItemActions><ng-icon name="lucideList" class="size-4 text-muted-foreground" /></div>
              </button>
            }
          </div>
        </div>
      </div>
    </div>
  `,
})
export class MenuDemoComponent {
    activeNav = 'active';
    activeItem = '1';

    readonly nav = [
        {id: 'attention', label: 'Needs attention', icon: 'lucideMegaphone', count: 2},
        {id: 'active', label: 'Active', icon: 'lucideLayoutGrid', count: 12},
        {id: 'ended', label: 'Ended', icon: 'lucideArchive', count: 41},
    ];

    readonly lineItems: Array<{id: string; title: string; meta: string; tone: Tone}> = [
        {id: '1', title: 'Line item 1', meta: 'Infoscreen · Praha', tone: 'success'},
        {id: '2', title: 'Line item 2', meta: 'Citylight · Brno', tone: 'warning'},
        {id: '3', title: 'Line item 3', meta: 'Pick a medium', tone: 'muted'},
    ];

    readonly cardItems = this.lineItems.slice(0, 2);
}