Menü

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.

Vorn ausgerichtet
Hinten ausgerichtet
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'),
)

Ein Eintrag ohne href rendert einen <button> — für eine Aktion, die auf der Seite passiert, statt einer Navigation.

Exportieren
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:

PropTypStandardBeschreibung
triggerChild—Sichtbares Label. Übergib Text, keinen gerenderten button().
iconChild—Markup vor dem Label, oder allein für einen reinen Icon-Auslöser.
labelstring—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.
triggerClassstring—Zusätzliche Klassen für den Auslöser statt für das umschließende details.

menuItem():

PropTypStandardBeschreibung
hrefstring—Rendert einen Anker; ohne ihn einen Button.
iconChild—Markup vor dem Label.
asstring'button'Element, das ohne href gerendert wird.

menuSeparator() nimmt keine Props — es ist die Haarlinie zwischen Gruppen von Einträgen.