# Menu

> A dropdown built on <details>, so it opens and closes with no script at all.

A menu is a `<details>` with a styled panel. That is a deliberate choice over the popover API: a popover lives in the top layer and cannot be positioned against its trigger without anchor positioning, which is not yet everywhere. A `<details>` positions itself correctly today and needs nothing loaded.

The trigger is that `<summary>`, styled as a button — so you pass the label and the button props to `menu()` rather than passing a rendered `button()`. A summary is already interactive, and a button inside one nests two controls where there is a single action: invalid markup, and two tab stops for one thing.

Close-on-outside-click and Escape come from an `ontoggle` handler that imports them the first time a menu is opened — and only then. If that module never arrives, a menu still opens and closes from its own summary.

## Basic menu

```javascript
menu({ trigger: 'Actions' },
  menuItem({ href: '#edit' }, 'Edit'),
  menuItem({ href: '#duplicate' }, 'Duplicate'),
  menuSeparator(),
  menuItem({ href: '#delete' }, 'Delete'),
)
```

## Alignment

A menu opens from the start edge of its trigger. `align: 'end'` flips it, which is what a menu near the right edge of a bar needs.

```javascript
stack({ direction: 'row', gap: 'xl', justify: 'space-between', style: 'width: 100%' },
  menu({ trigger: 'Start-aligned', variant: 'soft' },
    menuItem({ href: '#a' }, 'First'),
    menuItem({ href: '#b' }, 'Second'),
  ),
  menu({ trigger: 'End-aligned', variant: 'soft', align: 'end' },
    menuItem({ href: '#c' }, 'First'),
    menuItem({ href: '#d' }, 'Second'),
  ),
)
```

## Icon triggers

An icon with no `trigger` text needs a `label` — it becomes the accessible name the icon cannot provide.

```javascript
stack({ direction: 'row', gap: 'md' },
  menu({
    align: 'end',
    label: 'More actions',
    variant: 'ghost',
    icon: icon('more-horizontal'),
  },
    menuItem({ href: '#rename' }, 'Rename'),
    menuItem({ href: '#move' }, 'Move'),
    menuSeparator(),
    menuItem({ href: '#archive' }, 'Archive'),
  ),
)
```

## Items with icons

```javascript
menu({ trigger: 'File' },
  menuItem({
    href: '#new',
    icon: icon('plus'),
  }, 'New page'),
  menuItem({
    href: '#open',
    icon: icon('folder'),
  }, 'Open…'),
  menuSeparator(),
  menuItem({
    href: '#build',
    icon: icon('zap'),
  }, 'Build site'),
)
```

## Buttons instead of links

An item with no `href` renders a `<button>` — for an action that happens on the page rather than a navigation.

```javascript
menu({ trigger: 'Export', variant: 'soft', color: 'primary' },
  menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Exported as JSON.',{color:'success'}))" }, 'As JSON'),
  menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Exported as CSV.',{color:'success'}))" }, 'As CSV'),
)
```

## In an app bar

```javascript
appBar({ brand: 'sitelo' },
  appBarSpacer(),
  appBarActions(
    themeToggle(),
    menu({
      align: 'end',
      label: 'More',
      variant: 'ghost',
      icon: icon('more-horizontal'),
    },
      menuItem({ href: '/docs' }, 'Docs'),
      menuItem({ href: '/examples' }, 'Examples'),
      menuSeparator(),
      menuItem({ href: 'https://github.com/paul-browne/sitelo' }, 'GitHub'),
    ),
  ),
)
```

## Accessibility

The panel is a `role="menu"` whose items are `role="menuitem"`, and the summary carries `aria-haspopup`. A `<details>` is not a native menu widget, so this is a reasonable approximation rather than a perfect one — for a plain list of links, a `nav` inside the details is just as valid and claims less.

## Props

`menu()` — the trigger props are the button ones:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `trigger` | `Child` | — | Visible label. Pass text, not a rendered button(). |
| `icon` | `Child` | — | Markup before the label, or on its own for an icon-only trigger. |
| `label` | `string` | — | Accessible name. Required when there is an icon and no trigger text. |
| `variant` | `'solid' \| 'soft' \| 'outline' \| 'ghost' \| 'link'` | `'outline'` | Trigger styling. |
| `color` | `'primary' \| 'neutral' \| 'success' \| 'warning' \| 'danger'` | `'neutral'` | Which palette the trigger draws from. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Trigger size. |
| `align` | `'start' \| 'end'` | `'start'` | Which edge of the trigger the panel lines up with. |
| `triggerClass` | `string` | — | Extra classes for the trigger rather than the wrapping details. |

`menuItem()`:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `href` | `string` | — | Renders an anchor; without it, a button. |
| `icon` | `Child` | — | Markup before the label. |
| `as` | `string` | `'button'` | Element to render when there is no href. |

`menuSeparator()` takes no props — it is the hairline between groups of items.
