Skip to content
ShineUI
Install

Menu

The vertical action list.

<sui-menu>Menus and navigationStable

About

Vertical action menu: sections, icons, shortcuts, badges, danger and disabled rows. It paints rows only, so the surface it sits on is the one the reader sees: a sui-popover for a menu opened from a button, or variant="card" for a menu at rest on a page. Nested rows are the job of sui-tiered-menu.

Usage

<sui-menu variant="card" aria-label="Invoice actions"> <div slot="header"> <div style="font-size:13px;font-weight:700">INV-1042</div> <div style="font-size:11.5px;color:var(--ink-faint)">Riverbend Dental Group</div> </div> </sui-menu> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-menu"); el.items = [ { section: "Actions" }, { label: "Edit", icon: "fa-regular fa-pen-to-square", shortcut: "E" }, { label: "Duplicate", icon: "fa-regular fa-copy" }, { label: "Send reminder", icon: "fa-regular fa-paper-plane", badge: 2 }, { separator: true }, { label: "Void invoice", icon: "fa-regular fa-trash-can", danger: true }, ]; /* SuiMenuItem[] */ el.addEventListener("suiPick", (e) => console.log(e.detail)); </script>
import { SuiMenu } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const items = [ { section: "Actions" }, { label: "Edit", icon: "fa-regular fa-pen-to-square", shortcut: "E" }, { label: "Duplicate", icon: "fa-regular fa-copy" }, { label: "Send reminder", icon: "fa-regular fa-paper-plane", badge: 2 }, { separator: true }, { label: "Void invoice", icon: "fa-regular fa-trash-can", danger: true }, ]; /* SuiMenuItem[] */ return ( <SuiMenu variant="card" aria-label="Invoice actions" items={items} onSuiPick={(e) => console.log(e.detail)}> <div slot="header"> <div style="font-size:13px;font-weight:700">INV-1042</div> <div style="font-size:11.5px;color:var(--ink-faint)">Riverbend Dental Group</div> </div> </SuiMenu> ); }
<script setup> import { SuiMenu } from "@shineui/vue"; import "@shineui/core/shineui.css"; const items = [ { section: "Actions" }, { label: "Edit", icon: "fa-regular fa-pen-to-square", shortcut: "E" }, { label: "Duplicate", icon: "fa-regular fa-copy" }, { label: "Send reminder", icon: "fa-regular fa-paper-plane", badge: 2 }, { separator: true }, { label: "Void invoice", icon: "fa-regular fa-trash-can", danger: true }, ]; // SuiMenuItem[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiMenu variant="card" aria-label="Invoice actions" :items="items" @sui-pick="onChange"><div slot="header"> <div style="font-size:13px;font-weight:700">INV-1042</div> <div style="font-size:11.5px;color:var(--ink-faint)">Riverbend Dental Group</div> </div></SuiMenu> </template>
import { Component } from "@angular/core"; import { SuiMenu } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiMenu], template: `<sui-menu variant="card" aria-label="Invoice actions" [items]="items" (suiPick)="onChange($event)"><div slot="header"> <div style="font-size:13px;font-weight:700">INV-1042</div> <div style="font-size:11.5px;color:var(--ink-faint)">Riverbend Dental Group</div> </div></sui-menu>`, }) export class Example { items = [ { section: "Actions" }, { label: "Edit", icon: "fa-regular fa-pen-to-square", shortcut: "E" }, { label: "Duplicate", icon: "fa-regular fa-copy" }, { label: "Send reminder", icon: "fa-regular fa-paper-plane", badge: 2 }, { separator: true }, { label: "Void invoice", icon: "fa-regular fa-trash-can", danger: true }, ]; // SuiMenuItem[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The host aria-label names the role="menu". Each row is a role="menuitem"; a disabled row reads aria-disabled. variant="card" is the resting frame for a menu that stands on a page; the default paints rows only.

INV-1042
Riverbend Dental Group
<sui-menu variant="card" aria-label="Invoice actions"> <div slot="header"> <div style="font-size:13px;font-weight:700">INV-1042</div> <div style="font-size:11.5px;color:var(--ink-faint)">Riverbend Dental Group</div> </div> </sui-menu>
// setup(root): properties and events setup(root) { root.querySelector('sui-menu').items = rowActions(); }

Row vocabulary

One flat array carries every row kind: a section eyebrow, an icon, a keyboard shortcut, a count badge and a separator.

<sui-menu id="menu-rows" variant="card" aria-label="Account actions"></sui-menu>
// setup(root): properties and events setup(root) { root.querySelector('#menu-rows').items = [ { section: 'This period' }, { label: 'Reconcile', icon: 'fa-solid fa-check-double', shortcut: 'R' }, { label: 'Unmatched lines', icon: 'fa-regular fa-circle-question', badge: 14 }, { separator: true }, { section: 'Export' }, { label: 'Download CSV', icon: 'fa-regular fa-file-lines' }, { label: 'Download PDF', icon: 'fa-regular fa-file-pdf' }, ]; }

Danger and disabled rows

A danger row takes the danger tokens and still picks normally. A disabled row is inert: the pointer ignores it and the keyboard steps over it.

<sui-menu id="menu-states" variant="card" aria-label="Bill actions"></sui-menu>
// setup(root): properties and events setup(root) { root.querySelector('#menu-states').items = [ { label: 'Approve bill', icon: 'fa-regular fa-circle-check' }, { label: 'Schedule payment', icon: 'fa-regular fa-calendar', disabled: true }, { label: 'Add to batch', icon: 'fa-solid fa-layer-group' }, { separator: true }, { label: 'Delete bill', icon: 'fa-regular fa-trash-can', danger: true }, ]; }

A row with href is a real link: middle-click and copy link work, Enter follows it, and suiPick and command still run on the click first. target sends it to a new tab. current marks the page the reader is on, bold and aria-current="page", the way a site menu does.

<sui-menu id="menu-links" variant="card" aria-label="Account actions"></sui-menu>
// setup(root): properties and events setup(root) { root.querySelector('#menu-links').items = [ { label: 'Open account', icon: 'fa-regular fa-folder-open', href: '#/accounts/4402', current: true }, { label: 'Transactions', icon: 'fa-regular fa-rectangle-list', href: '#/accounts/4402/transactions', badge: 14 }, { separator: true }, { label: 'Help center', icon: 'fa-regular fa-circle-question', href: 'https://example.com/help', target: '_blank' }, { label: 'Close account', icon: 'fa-regular fa-circle-xmark', danger: true }, ]; }

Width

width sets a card menu in px. Widen it for long destination names, narrow it for a compact row menu. A plain menu fills the box it sits in instead, so a popover sets the width once.

<div style="display:flex;gap:16px;align-items:flex-start"> <sui-menu id="menu-narrow" variant="card" width="170" aria-label="Compact actions"></sui-menu> <sui-menu id="menu-wide" variant="card" width="290" aria-label="Posting actions"></sui-menu> </div>
// setup(root): properties and events setup(root) { root.querySelector('#menu-narrow').items = [ { label: 'Edit', icon: 'fa-regular fa-pen-to-square' }, { label: 'Copy', icon: 'fa-regular fa-copy' }, ]; root.querySelector('#menu-wide').items = [ { label: 'Post to the general ledger', icon: 'fa-solid fa-book' }, { label: 'Move to a later accounting period', icon: 'fa-regular fa-calendar' }, ]; }

In a popover

The menu has no popup mode: a sui-popover with flush and menu gives it a trigger and is the one card around the rows. The trigger announces a menu, opening puts focus on the first row so the arrows work at once, and a suiPick listener closes it.

<div style="min-height:270px"> <sui-popover id="menu-pop" width="240" flush menu> <sui-button slot="trigger" variant="outline" label="Actions" icon="fa-solid fa-ellipsis"></sui-button> <sui-menu id="menu-pop-menu" aria-label="Invoice actions"></sui-menu> </sui-popover> </div>
// setup(root): properties and events setup(root) { const pop = root.querySelector('#menu-pop'); const el = root.querySelector('#menu-pop-menu'); el.items = rowActions(); el.addEventListener('suiPick', () => { pop.open = false; }); }

Events

A pick emits suiPick with the item and then runs the row command. Both happen, so a framework can listen while plain HTML keeps the callback.

Nothing picked yet.
<div style="display:flex;gap:16px;align-items:flex-start"> <sui-menu id="menu-events" variant="card" aria-label="Invoice actions"></sui-menu> <span id="menu-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Nothing picked yet.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#menu-log'); const el = root.querySelector('#menu-events'); el.items = [ { label: 'Edit', icon: 'fa-regular fa-pen-to-square', shortcut: 'E' }, { label: 'Duplicate', icon: 'fa-regular fa-copy' }, { label: 'Mark as paid', icon: 'fa-regular fa-circle-check', command() { log.textContent += ' Its command ran.'; }, }, { separator: true }, { label: 'Void invoice', icon: 'fa-regular fa-trash-can', danger: true }, ]; el.addEventListener('suiPick', (e) => { log.textContent = `Picked ${e.detail.label}.`; }); }

Keyboard

KeyAction
TabThe menu is one tab stop: Tab lands on the first enabled row and leaves on the next press.
ArrowDown ArrowUpMoves to the next or previous row, wrapping. Separators, section eyebrows and disabled rows are skipped.
Home EndMoves to the first or last enabled row.
Enter SpacePicks the focused row: emits suiPick and runs its command. On a row with href the browser then follows the link.
Any letterMoves to the next row whose label starts with that letter.

API

<sui-menu>

Properties

PropertyAttributeTypeDefaultDescription
itemsproperty onlySuiMenuItem[][]Rows: { label, icon, shortcut, badge, danger, disabled, separator, section, command, href, target, current }. A row with href is a real link with the same styling and keyboard reach: Enter follows it, and suiPick and command still run on the click. A row with current is the page the reader is on: bold, and aria-current="page". A row's nested items are not opened here and the row says nothing about them: use sui-tiered-menu for a tree. Property, not attribute. Put an aria-label on the host to name the menu; a row menu beside a table row is otherwise announced with no context.
variantvariant"card" | "plain"'plain'Surface the menu paints. plain is the default: rows only, no border and no shadow, because a menu is a list and whatever floats it paints the card. A sui-popover with flush does that for a menu opened from a button; sui-context-menu and sui-split-button do it for their own menus. card adds a resting border and rounded corners for a menu that stands on a page by itself, with no shadow, since nothing at rest casts one. Anything else falls back to plain.
widthwidthnumber230Width of a card menu in px. A plain menu takes the width of the box it sits in, a popover panel or a card body, and ignores this.

Events

EventDetailReactVueDescription
suiPickSuiMenuItemonSuiPick@sui-pickFires when an enabled row is picked; detail is the item. item.command() also runs. On a link row both happen on the click, before the browser follows the href.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the menu's one tab stop: the highlighted row, else the first enabled one.

Slots

SlotDescription
headerOptional header above the rows (e.g. the record id and its owner).

Types

SuiMenuItem interface
label?string
icon?string
shortcut?string
badge?string | number
danger?boolean
disabled?boolean
separator?boolean
section?stringSection heading row (uppercase eyebrow).
items?SuiMenuItem[]Nested items (TieredMenu, Menubar dropdowns).
command?() => void
href?stringRenders the row as a real link, so middle-click, copy link and open in a new tab work and Enter follows it. The row keeps its styling, its place in the arrow-key order and its pick: command runs and suiPick fires on the click, then the browser navigates. Ignored on a disabled row and on a row with nested items; those stay buttons.
target?stringLink target for a row with href, e.g. _blank. A _blank row gets rel="noopener".
current?booleanThe row is the page the reader is on: it carries aria-current="page" and the bold face sui-side-nav gives its current item. For a navigation menu, the site pages behind a phone menu button; an action menu has no current row.
checked?booleanMakes the row one choice of a set, for a mode or an order picked from a menu: the chosen row draws a check and every row keeps room for one, and the rows read as menuitemradio with aria-checked. Leave it unset on an action row.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also