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. command="toggle-popover" and commandfor on the trigger open it, with no script involved, 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">
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
Where a trigger cannot carry commandfor, open and close the menu imperatively
instead. 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 where you have the choice, and
leave bind:open to report the state rather than to set it.
<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>| Method | Type | Description |
|---|---|---|
show | () => void | Opens the menu. |
close | () => 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 when the menu would not fit, and slides along the inline axis when it would run off the edge of the window. When neither side has room, the menu takes the full height of the window and sits over the anchor instead of squeezing into whichever gap is bigger. Only a menu taller than the window stays on the roomier side and scrolls, since covering the anchor would not help 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 that 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 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. Room on screen is measured against it, which 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.