Menü
Auf dieser Seite
Ein Menü ist ein <details> mit einem gestalteten Panel. Das ist eine bewusste Entscheidung gegen die Popover-API: ein Popover lebt in der obersten Ebene und lässt sich ohne Anchor Positioning nicht an seinem Auslöser ausrichten — und das gibt es noch nicht überall. Ein <details> positioniert sich heute schon richtig und braucht nichts Geladenes.
Der Auslöser ist genau dieses <summary>, gestaltet wie ein Button — du übergibst Label und Button-Props also an menu(), statt einen fertig gerenderten button() hineinzureichen. Ein summary ist bereits interaktiv, und ein Button darin schachtelt zwei Bedienelemente ineinander, wo es nur eine Aktion gibt: ungültiges Markup und zwei Tab-Stopps für eine Sache.
Schließen bei Klick nach außen und mit Escape kommt aus einem ontoggle-Handler, der beides beim ersten Öffnen eines Menüs importiert — und erst dann. Kommt dieses Modul nie an, geht ein Menü über sein eigenes summary trotzdem auf und zu.
Einfaches Menü
menu({ trigger: 'Aktionen' },
menuItem({ href: '#edit' }, 'Bearbeiten'),
menuItem({ href: '#duplicate' }, 'Duplizieren'),
menuSeparator(),
menuItem({ href: '#delete' }, 'Löschen'),
)Ausrichtung
Ein Menü klappt an der vorderen Kante seines Auslösers auf. align: 'end' dreht das um — genau das braucht ein Menü nahe am rechten Rand einer Leiste.
stack({ direction: 'row', gap: 'xl', justify: 'space-between', style: 'width: 100%' },
menu({ trigger: 'Vorn ausgerichtet', variant: 'soft' },
menuItem({ href: '#a' }, 'Erstes'),
menuItem({ href: '#b' }, 'Zweites'),
),
menu({ trigger: 'Hinten ausgerichtet', variant: 'soft', align: 'end' },
menuItem({ href: '#c' }, 'Erstes'),
menuItem({ href: '#d' }, 'Zweites'),
),
)Icon-Auslöser
Ein Icon ohne trigger-Text braucht ein label — daraus wird der zugängliche Name, den das Icon nicht liefern kann.
stack({ direction: 'row', gap: 'md' },
menu({
align: 'end',
label: 'Weitere Aktionen',
variant: 'ghost',
icon: icon('more-horizontal'),
},
menuItem({ href: '#rename' }, 'Umbenennen'),
menuItem({ href: '#move' }, 'Verschieben'),
menuSeparator(),
menuItem({ href: '#archive' }, 'Archivieren'),
),
)Einträge mit Icons
menu({ trigger: 'Datei' },
menuItem({
href: '#new',
icon: icon('plus'),
}, 'Neue Seite'),
menuItem({
href: '#open',
icon: icon('folder'),
}, 'Öffnen…'),
menuSeparator(),
menuItem({
href: '#build',
icon: icon('zap'),
}, 'Website bauen'),
)Buttons statt Links
Ein Eintrag ohne href rendert einen <button> — für eine Aktion, die auf der Seite passiert, statt einer Navigation.
menu({ trigger: 'Exportieren', variant: 'soft', color: 'primary' },
menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Als JSON exportiert.',{color:'success'}))" }, 'Als JSON'),
menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Als CSV exportiert.',{color:'success'}))" }, 'Als CSV'),
)In einer App-Bar
appBar({ brand: 'sitelo' },
appBarSpacer(),
appBarActions(
themeToggle(),
menu({
align: 'end',
label: 'Mehr',
variant: 'ghost',
icon: icon('more-horizontal'),
},
menuItem({ href: '/de/docs' }, 'Doku'),
menuItem({ href: '/de/examples' }, 'Beispiele'),
menuSeparator(),
menuItem({ href: 'https://github.com/paul-browne/sitelo' }, 'GitHub'),
),
),
)Barrierefreiheit
Das Panel ist ein role="menu", dessen Einträge role="menuitem" sind, und das summary trägt aria-haspopup. Ein <details> ist kein natives Menü-Widget, das hier ist also eine vernünftige Annäherung und keine perfekte — für eine schlichte Linkliste ist ein nav im details genauso gültig und verspricht weniger.
Props
menu() — die Auslöser-Props sind die des Buttons:
| Prop | Typ | Standard | Beschreibung |
|---|---|---|---|
trigger | Child | — | Sichtbares Label. Übergib Text, keinen gerenderten button(). |
icon | Child | — | Markup vor dem Label, oder allein für einen reinen Icon-Auslöser. |
label | string | — | Zugänglicher Name. Pflicht, wenn es ein Icon und keinen trigger-Text gibt. |
variant | 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'outline' | Gestaltung des Auslösers. |
color | 'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'neutral' | Aus welcher Palette der Auslöser schöpft. |
size | 'sm' | 'md' | 'lg' | 'md' | Größe des Auslösers. |
align | 'start' | 'end' | 'start' | An welcher Kante des Auslösers das Panel ausgerichtet wird. |
triggerClass | string | — | Zusätzliche Klassen für den Auslöser statt für das umschließende details. |
menuItem():
| Prop | Typ | Standard | Beschreibung |
|---|---|---|---|
href | string | — | Rendert einen Anker; ohne ihn einen Button. |
icon | Child | — | Markup vor dem Label. |
as | string | 'button' | Element, das ohne href gerendert wird. |
menuSeparator() nimmt keine Props — es ist die Haarlinie zwischen Gruppen von Einträgen.