Menus

A menu shows a list of choices on a temporary surface, anchored to the control that opened it. It is a native popover in the top layer, so a scroll container or an overflow: hidden ancestor does not clip it. The browser closes it on a click outside or on Escape.

command="toggle-popover" and commandfor on the trigger open the menu without script. 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 left on screen.

Usage

<script lang="ts">
	import { Button, Menu, MenuItem } from 'noph-ui'
	let menuBtn: HTMLElement | undefined = $state()
</script>

<Button
	style="anchor-name:--city-menu"
	command="toggle-popover"
	commandfor="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

When a trigger cannot have commandfor, open and close the menu with methods. Bind a reference with bind:this and type it with ReturnType<typeof Menu>. It is undefined until the component has mounted, so call through ?.. Prefer the attributes when you can.

<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?.show()}>Open menu</Button>
MethodTypeDescription
show() => voidOpens the menu.
close() => voidCloses 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 from the menu.

For a choice, set role="menuitemradio" when one of several options is on, or role="menuitemcheckbox" when each option toggles on its own. Both announce selected as checked.

<Menu anchor={sortBtn} id="sort-menu">
	{#each ['Name', 'Date', 'Size'] as option (option)}
		<MenuItem
			role="menuitemradio"
			selected={sort === option}
			command="hide-popover"
			commandfor="sort-menu"
			onclick={() => (sort = option)}
		>
			{option}
		</MenuItem>
	{/each}
</Menu>
<Menu anchor={menuBtn} id="account-menu">
	<MenuItem>
		Profile
		{#snippet start()}<Icon>person</Icon>{/snippet}
	</MenuItem>
	<MenuItem href={resolve('/settings')}>Settings</MenuItem>
	<Divider />
	<MenuItem disabled>Sign out</MenuItem>
</Menu>

Placement

A menu opens on the side of the anchor you set, below it by default. When that side is too small, it flips to the opposite side. When it would run off the edge of the window, it slides along the inline axis. When neither side has room, the menu takes the full height of the window and sits over the anchor. A menu taller than the window stays on the side with more room and scrolls.

The menu measures the room against the anchor prop. Without it the menu still opens, but at the height of its content.

Set coverAnchor to false when the anchor has to stay visible. The menu then stays on the side with more room and scrolls there. AutoComplete does this, so the list does not hide what you are typing.

Accessibility

The container renders role="menu" and every item role="menuitem", or the radio or checkbox role you give it. Opening the menu moves focus to the first item. The items share one tab stop. Tab moves into and out of the menu. ↑ and ↓ move between the items and wrap around at the ends. Home and End jump to the first and the last item. Typing a letter jumps to the next item that starts with it. Enter and Space pick the focused item, links included. Disabled items still take focus, so screen readers read them, but they cannot be picked.

Escape closes the menu and focus returns to the trigger. Give the trigger an accessible name that says what the menu is for, not only “Open menu”.

Theming

TokenDefault 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-elevation--np-elevation-2
--np-menu-margin2px
--np-menu-position-areabottom
--np-menu-justify-selfanchor-center
--np-menu-over-anchor-position-areaspan-all
--np-menu-min-width7rem
--np-menu-max-width17.5rem
--np-menu-item-container-height3rem
--np-menu-item-padding-inline0.75rem

--np-menu-position-area takes any CSS position-area value and sets which side of the anchor the menu opens on. When that side is too small, the menu still moves as Placement describes. --np-menu-over-anchor-position-area is the area for the last fallback, when the menu spans the full height over the anchor. Keep span-all in the block axis and repeat the inline half of --np-menu-position-area, so the menu keeps the same alignment. Style the items with 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

Other attributes, such as id, class, style and event handlers, go to the menu element.

AttributeTypeDefaultDescription
anchorHTMLElement | undefinedundefinedThe element the menu belongs to. The menu measures the room on screen against it to set its height and where it moves when a side is too small.
openboolean | undefinedundefinedBindable. Whether the menu is open.
coverAnchorbooleantrueWhether the menu may sit over its anchor when neither side of it is tall enough. With false the menu stays on the side with more room and scrolls there.
popover'auto' | 'manual' | null'auto'Popover behaviour. auto closes on an outside click and on Escape. Use manual to control closing yourself.
elementHTMLDivElement | undefinedundefinedBindable reference to the menu element.

MenuItem accepts the ListItem attributes apart from variant and softFocus, which the menu sets itself.