Skip to content
ShineUI
Install

Menubar

Horizontal menus with depth.

<sui-menubar>Menus and navigationStable

About

Horizontal menubar for lighter surfaces such as a client portal: a brand slot at the start, top items with dropdowns, and an end slot for the account. Rows nest to any depth, a top item without rows is a plain action, and what does not fit folds into More. aria-label or aria-labelledby on the host names it.

Usage

<sui-menubar aria-label="Main"> <b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <span slot="end" style="font:460 12.5px var(--font-sans);color:var(--ink-muted)">Dana Whitfield</span> </sui-menubar> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-menubar"); el.items = [ { label: "Dashboard", icon: "fa-regular fa-chart-bar" }, { label: "Sales", items: [ { label: "Customers", icon: "fa-regular fa-user" }, { label: "Invoices", icon: "fa-regular fa-file-lines", badge: 9 }, { label: "Credit notes", icon: "fa-regular fa-file" }, { separator: true }, { label: "New invoice", icon: "fa-solid fa-plus", shortcut: "N" }, ], }, { label: "Reports", items: [ { section: "Statements" }, { label: "Profit and loss", icon: "fa-regular fa-chart-bar" }, { label: "Balance sheet", icon: "fa-regular fa-file-lines" }, { label: "Trial balance", icon: "fa-regular fa-rectangle-list", disabled: true }, { separator: true }, { label: "Delete saved report", icon: "fa-regular fa-trash-can", danger: true }, ], }, ]; /* SuiMenubarItem[] */ el.addEventListener("suiPick", (e) => console.log(e.detail)); </script>
import { SuiMenubar } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const items = [ { label: "Dashboard", icon: "fa-regular fa-chart-bar" }, { label: "Sales", items: [ { label: "Customers", icon: "fa-regular fa-user" }, { label: "Invoices", icon: "fa-regular fa-file-lines", badge: 9 }, { label: "Credit notes", icon: "fa-regular fa-file" }, { separator: true }, { label: "New invoice", icon: "fa-solid fa-plus", shortcut: "N" }, ], }, { label: "Reports", items: [ { section: "Statements" }, { label: "Profit and loss", icon: "fa-regular fa-chart-bar" }, { label: "Balance sheet", icon: "fa-regular fa-file-lines" }, { label: "Trial balance", icon: "fa-regular fa-rectangle-list", disabled: true }, { separator: true }, { label: "Delete saved report", icon: "fa-regular fa-trash-can", danger: true }, ], }, ]; /* SuiMenubarItem[] */ return ( <SuiMenubar aria-label="Main" items={items} onSuiPick={(e) => console.log(e.detail)}> <b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <span slot="end" style="font:460 12.5px var(--font-sans);color:var(--ink-muted)">Dana Whitfield</span> </SuiMenubar> ); }
<script setup> import { SuiMenubar } from "@shineui/vue"; import "@shineui/core/shineui.css"; const items = [ { label: "Dashboard", icon: "fa-regular fa-chart-bar" }, { label: "Sales", items: [ { label: "Customers", icon: "fa-regular fa-user" }, { label: "Invoices", icon: "fa-regular fa-file-lines", badge: 9 }, { label: "Credit notes", icon: "fa-regular fa-file" }, { separator: true }, { label: "New invoice", icon: "fa-solid fa-plus", shortcut: "N" }, ], }, { label: "Reports", items: [ { section: "Statements" }, { label: "Profit and loss", icon: "fa-regular fa-chart-bar" }, { label: "Balance sheet", icon: "fa-regular fa-file-lines" }, { label: "Trial balance", icon: "fa-regular fa-rectangle-list", disabled: true }, { separator: true }, { label: "Delete saved report", icon: "fa-regular fa-trash-can", danger: true }, ], }, ]; // SuiMenubarItem[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiMenubar aria-label="Main" :items="items" @sui-pick="onChange"><b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <span slot="end" style="font:460 12.5px var(--font-sans);color:var(--ink-muted)">Dana Whitfield</span></SuiMenubar> </template>
import { Component } from "@angular/core"; import { SuiMenubar } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiMenubar], template: `<sui-menubar aria-label="Main" [items]="items" (suiPick)="onChange($event)"><b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <span slot="end" style="font:460 12.5px var(--font-sans);color:var(--ink-muted)">Dana Whitfield</span></sui-menubar>`, }) export class Example { items = [ { label: "Dashboard", icon: "fa-regular fa-chart-bar" }, { label: "Sales", items: [ { label: "Customers", icon: "fa-regular fa-user" }, { label: "Invoices", icon: "fa-regular fa-file-lines", badge: 9 }, { label: "Credit notes", icon: "fa-regular fa-file" }, { separator: true }, { label: "New invoice", icon: "fa-solid fa-plus", shortcut: "N" }, ], }, { label: "Reports", items: [ { section: "Statements" }, { label: "Profit and loss", icon: "fa-regular fa-chart-bar" }, { label: "Balance sheet", icon: "fa-regular fa-file-lines" }, { label: "Trial balance", icon: "fa-regular fa-rectangle-list", disabled: true }, { separator: true }, { label: "Delete saved report", icon: "fa-regular fa-trash-can", danger: true }, ], }, ]; // SuiMenubarItem[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The bar is role="menubar" and its top items are one tab stop. Name it with aria-label or aria-labelledby on the host; the element hands the name to the menubar. A top item with rows reads aria-haspopup="true" and aria-expanded, and its dropdown is a role="menu" named after it.

Workspace Dana Whitfield
<sui-menubar aria-label="Main"> <b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <span slot="end" style="font:460 12.5px var(--font-sans);color:var(--ink-muted)">Dana Whitfield</span> </sui-menubar>
// setup(root): properties and events setup(root) { root.querySelector('sui-menubar').items = barItems(); }

Actions without a dropdown

A top item with no rows is a plain action. It draws no chevron, reads aria-haspopup="false" and emits suiPick on click.

Workspace No action yet.
<sui-menubar id="mb-mixed"> <b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <span id="mb-mixed-log" slot="end" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">No action yet.</span> </sui-menubar>
// setup(root): properties and events setup(root) { const log = root.querySelector('#mb-mixed-log'); const el = root.querySelector('#mb-mixed'); el.items = [ { label: 'Dashboard', icon: 'fa-regular fa-chart-bar' }, { label: 'Sales', items: salesRows() }, { label: 'Help', icon: 'fa-regular fa-circle-question' }, ]; el.addEventListener('suiPick', (e) => { log.textContent = `Picked ${e.detail.label}.`; }); }

Submenus

A dropdown row with its own items opens a flyout beside it, to any depth. The row draws a chevron and reads aria-haspopup="menu" with aria-expanded, and a row with children is never a link.

Workspace Dana Whitfield
<div style="min-height:300px"> <sui-menubar id="mb-sub" open-index="1"> <b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <span slot="end" style="font:460 12.5px var(--font-sans);color:var(--ink-muted)">Dana Whitfield</span> </sui-menubar> </div>
// setup(root): properties and events setup(root) { root.querySelector('#mb-sub').items = [ { label: 'Dashboard', icon: 'fa-regular fa-chart-bar' }, { label: 'Reports', items: [ { section: 'Statements' }, { label: 'Profit and loss', icon: 'fa-regular fa-chart-bar' }, { label: 'Balance sheet', icon: 'fa-regular fa-file-lines' }, { separator: true }, { label: 'Export', icon: 'fa-regular fa-share-from-square', items: [ { label: 'PDF', icon: 'fa-regular fa-file-pdf' }, { label: 'Excel', icon: 'fa-regular fa-file-excel' }, { label: 'Accounting formats', icon: 'fa-solid fa-ellipsis', items: [ { label: 'QuickBooks IIF' }, { label: 'Xero CSV' }, { label: 'Sage 50', disabled: true }, ], }, ], }, ], }, { label: 'Help', icon: 'fa-regular fa-circle-question' }, ]; }

A top item without rows, or a dropdown row, takes href and becomes a real link with the same keyboard reach. suiPick still fires on the click, then the browser follows.

Workspace Dana Whitfield
<sui-menubar id="mb-links"> <b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <span slot="end" style="font:460 12.5px var(--font-sans);color:var(--ink-muted)">Dana Whitfield</span> </sui-menubar>
// setup(root): properties and events setup(root) { root.querySelector('#mb-links').items = [ { label: 'Dashboard', icon: 'fa-regular fa-chart-bar', href: '#/dashboard' }, { label: 'Sales', items: [ { label: 'Customers', icon: 'fa-regular fa-user', href: '#/customers' }, { label: 'Invoices', icon: 'fa-regular fa-file-lines', href: '#/invoices', badge: 9 }, { separator: true }, { label: 'New invoice', icon: 'fa-solid fa-plus', shortcut: 'N' }, ], }, { label: 'Help', icon: 'fa-regular fa-circle-question', href: 'https://example.com/help', target: '_blank' }, ]; }

Folding into More

Top items that do not fit fold into More at the end of the bar, starting from the last. More is a top item like the others. In its menu a folded item keeps its rows as a submenu, which drills in on a phone. More is index items.length in open-index and suiOpen. Set no-fold to keep every item.

Workspace
<div style="max-width:380px;min-height:260px"> <sui-menubar id="mb-fold"> <b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <sui-avatar slot="end" name="Dana Whitfield" size="sm"></sui-avatar> </sui-menubar> </div>
// setup(root): properties and events setup(root) { root.querySelector('#mb-fold').items = [...barItems(), { label: 'Help', icon: 'fa-regular fa-circle-question' }]; }

Open dropdown

Write open-index to pin a dropdown, or read it to know which one is open. While one is open, hovering another top item switches to it.

Workspace Dana Whitfield
<div style="min-height:280px"> <sui-menubar id="mb-open" open-index="2"> <b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <span slot="end" style="font:460 12.5px var(--font-sans);color:var(--ink-muted)">Dana Whitfield</span> </sui-menubar> </div>
// setup(root): properties and events setup(root) { root.querySelector('#mb-open').items = barItems(); }

Events

suiOpen carries the index of the top item that opened, and a pick emits suiPick and then runs the row command. suiClose fires before the dropdown closes with the reason (trigger, outside, escape or pick) and is cancelable, so preventDefault keeps it open.

Workspace Closed.
<div style="min-height:240px"> <sui-menubar id="mb-events"> <b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <span id="mb-log" slot="end" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Closed.</span> </sui-menubar> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#mb-log'); const el = root.querySelector('#mb-events'); const items = barItems(); items[1].items[4].command = () => { log.textContent += ' Its command ran.'; }; el.items = items; el.addEventListener('suiOpen', (e) => { log.textContent = `Opened item ${e.detail}.`; }); el.addEventListener('suiClose', (e) => { log.textContent = `Closed by ${e.detail}.`; }); el.addEventListener('suiPick', (e) => { log.textContent = `Picked ${e.detail.label}.`; }); }

Keyboard

KeyAction
TabLeaves the bar; the top items are one tab stop. An open dropdown closes first.
ArrowRightOn a top item, moves to the next one, wrapping. On a dropdown row with a submenu, opens it and lands on its first row; on any other row of the top level, moves to the next top item. Under dir="rtl" the pair swaps, so the arrow that points along the reading direction is always the forward one.
ArrowLeftOn a top item, moves to the previous one. Inside a submenu, closes it and returns focus to the row that opened it; on the top level of a dropdown, moves to the previous top item.
Home EndMoves to the first or last top item. While items are folded, More is the last one. Inside a dropdown or a submenu, moves to that level's first or last row.
ArrowDownOpens the focused item's dropdown on its first row. Inside a level, moves down a row, wrapping and skipping separators, section eyebrows and disabled rows.
ArrowUpOpens the focused item's dropdown on its last row. Inside a level, moves up a row.
Enter SpaceOn a top item, opens its dropdown on its first row, or closes it, or runs a plain action. Inside a dropdown, picks the row, or opens its submenu when it has one. On a top item or row with href the browser then follows the link.
EscapeCloses the dropdown and every submenu inside it, and returns focus to the top item.
Any letterMoves to the next row whose label starts with that letter, in the level that has focus. Typeahead never crosses into another level.

API

<sui-menubar>

Properties

PropertyAttributeTypeDefaultDescription
itemsproperty onlySuiMenubarItem[][]Top items: { label, icon?, items?: SuiMenuItem[], command?, href?, target? }[]. A top item or a dropdown row with href is a real link with the same keyboard reach. A dropdown row with its own items opens a flyout beside it, to any depth. Property, not attribute.
noFoldno-foldbooleanfalseFolds the top items that do not fit into a More item at the end of the bar. The bar measures its room as it resizes and folds from the end; the brand and end slots stay whole. In More's menu, a folded item with rows opens them as a submenu and a plain item is a row that runs it. noFold keeps every item in the bar, and a narrow screen cuts the last ones off.
openIndexopen-indexnumber-1Index of the open dropdown, -1 when closed. Mutable: the element writes it on click, hover and keyboard and reports each change through suiOpen and suiClose. While one dropdown is open, hovering another top item switches to it. An open dropdown paints in the browser's top layer, so a transformed, contained or clipping ancestor cannot move it or cut it off. While items are folded, More is index items.length, one past the last item, and writing the index of a folded item opens More on that item's row.

Events

EventDetailReactVueDescription
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the element closes the open dropdown; detail is 'trigger' | 'outside' | 'escape' | 'pick'. Cancelable: preventDefault() keeps it open (and cancels a switch to another dropdown). It does not bubble.
suiOpennumberonSuiOpen@sui-openFires after the element opens a dropdown; detail is the top-item index it opened, or items.length for More. It does not bubble.
suiPickSuiMenuItemonSuiPick@sui-pickFires when a dropdown row (or a top item without a dropdown) is picked; detail is the item. item.command() also runs.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the bar's one tab stop: the top item that last held focus, else the first, or More when that item is folded.

Slots

SlotDescription
brandBrand mark or product name at the start of the bar.
endContent at the end of the bar (avatar, actions).

Types

SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.
SuiMenubarItem interface
labelstring
icon?stringFont Awesome class shown before the label.
items?SuiMenuItem[]Dropdown rows (shared SuiMenuItem shape). A row may carry its own items for a submenu. Without them the top item is a plain action.
command?() => voidRuns when a top item without a dropdown is picked.
href?stringRenders a top item without a dropdown as a real link. Enter and Space follow it, and suiPick and command still run on the click. Ignored on an item with items.
target?stringLink target for a top item with href, e.g. _blank.
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