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