Menus
A menu shows a list of choices on a temporary surface, anchored to the control that opened it. It
is a native popover, so it lives in the top layer and is not clipped by a scroll container or an overflow: hidden ancestor, and the browser closes it when you click outside or press Escape.
Two things connect a menu to its anchor. popovertarget on the trigger opens it, and
CSS anchor positioning places it: give the trigger an anchor-name and point the menu
at it with position-anchor. Pass the same element as the anchor prop so the
menu can size itself to the space that is actually left on screen.
Usage
<script lang="ts">
let menuBtn = $state<HTMLElement>()
</script>
<Button style="anchor-name:--city-menu" popovertarget="browser-menu" bind:element={menuBtn}>
Open menu
</Button>
<Menu anchor={menuBtn} id="browser-menu" style="position-anchor:--city-menu;max-width: 300px">
<MenuItem>New York</MenuItem>
<MenuItem>Los Angeles</MenuItem>
<MenuItem>Berlin</MenuItem>
<MenuItem>London</MenuItem>
</Menu>Methods
Besides the native popovertarget attribute, you can open and close the menu
imperatively. Bind a reference with bind:this and type it with ReturnType<typeof Menu>; it is undefined until the component has
mounted, so call through ?..
<script lang="ts">
let menu: ReturnType<typeof Menu> | undefined = $state()
</script>
<Menu bind:this={menu} anchor={menuBtn}>
<MenuItem>New York</MenuItem>
</Menu>
<Button onclick={() => menu?.showPopover()}>Open menu</Button>| Method | Type | Description |
|---|---|---|
showPopover | () => void | Opens the menu. |
hidePopover | () => void | Closes the menu. |
Items
MenuItem is a list item with role="menuitem". It takes the same start, end, supportingText, selected and disabled attributes as ListItem. Pass an href and the item
renders as a link instead of a button. A Divider between items gets its spacing automatically.
<Menu anchor={menuBtn} id="account-menu">
<MenuItem>
Profile
{#snippet start()}<Icon>person</Icon>{/snippet}
</MenuItem>
<MenuItem href="/settings">Settings</MenuItem>
<Divider />
<MenuItem disabled>Sign out</MenuItem>
</Menu>Placement
A menu opens on the side of the anchor you asked for, below it by default, and moves out of the way on its own when that side is too small. It flips to the opposite side of the anchor when the menu would not fit there, and to the other side of it along the inline axis when the menu would run off the edge of the window. When neither side of the anchor is tall enough, the menu takes the whole height of the window and sits over the anchor, so a tall menu is shown in full instead of being squeezed into the larger of two small gaps. Only a menu longer than the window itself stays on the roomier side and scrolls, since covering the anchor would not buy anything there.
The anchor prop is what makes this work: it is the element the menu measures the room against.
Without it the menu still opens, but at whatever height its content asks for.
Set coverAnchor to false where the anchor has to stay in sight, and the
menu skips that last step: it keeps to the roomier side and scrolls there instead. AutoComplete does this, because a list over the field would hide what you are typing.
Accessibility
The container renders role="menu" and every item role="menuitem". The
items share a single tab stop: Tab moves into and out of the menu as a whole, while ↑ and ↓ move between the items and wrap around at the ends. Home and End jump to the first and the last item. Disabled items are skipped.
Because the menu is a popover, Escape closes it and focus returns to the trigger without any code of your own. Give the trigger an accessible name that says what the menu is for, not just “Open menu”.
Theming
| Token | Default value |
|---|---|
--np-menu-container-color | --np-color-surface-container |
--np-menu-text-color | --np-color-on-surface |
--np-menu-container-shape | --np-shape-corner-extra-small |
--np-menu-margin | 2px |
--np-menu-position-area | bottom |
--np-menu-justify-self | anchor-center |
--np-menu-over-anchor-position-area | span-all |
--np-menu-position-area takes any CSS position-area value and decides
which side of the anchor the menu opens on. Whatever you pick, the menu still moves as Placement describes when that side is too small. --np-menu-over-anchor-position-area is the area used for the last of those steps, the
one that spans the height over the anchor; keep span-all in the block axis and repeat
whatever the inline half of --np-menu-position-area says, so the menu stays lined up
the same way. The items themselves are styled through the list tokens.
Example
<Menu
anchor={menuBtn}
id="themed-menu"
--np-menu-container-color="var(--np-color-surface-container-highest)"
--np-menu-container-shape="1rem"
--np-menu-position-area="top"
>
<MenuItem>New York</MenuItem>
</Menu>API
Menu attributes
Everything else you pass is forwarded to the menu element, so id, class, style and the usual event handlers work as expected.
| Attribute | Type | Default | Description |
|---|---|---|---|
anchor | HTMLElement | undefined | undefined | The element the menu belongs to. It is what the room on screen is measured around, so it decides how tall the menu may be and which way it moves when a side is too small. |
open | boolean | undefined | undefined | Bindable. Reflects whether the menu is currently open. |
coverAnchor | boolean | true | Whether the menu may sit over its anchor when neither side of it is tall enough. With false the menu stays on the roomier side and scrolls there. |
popover | 'auto' | 'manual' | null | 'auto' | Popover behaviour. auto closes on an outside click and on Escape; use manual when you want to control that yourself. |
element | HTMLDivElement | undefined | undefined | Bindable reference to the menu element. |
MenuItem attributes
MenuItem accepts the ListItem attributes apart from variant and softFocus, which the menu sets itself.