Skip to content
ShineUI
Install

MegaMenu

A dropdown with columns.

<sui-mega-menu>Menus and navigationStable

About

Menubar whose dropdown is a panel of columns with headings and descriptions, for feature-rich navigation such as a portal home. A top item without columns is a plain action, such as Pricing beside Products. orientation stacks it as a sidebar; a horizontal bar folds what does not fit into More. aria-label or aria-labelledby on the host names it.

Usage

<sui-mega-menu aria-label="Products"> <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-mega-menu> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-mega-menu"); el.items = [ { label: "Accounting", items: [ { heading: "Money in", links: [ { label: "Invoices", icon: "fa-regular fa-file-lines", description: "Bill your customers" }, { label: "Payments", icon: "fa-regular fa-circle-check", description: "Receive and deposit" }, ], }, { heading: "Books", links: [ { label: "Chart of accounts", icon: "fa-regular fa-rectangle-list", description: "32 accounts" }, { label: "General ledger", icon: "fa-solid fa-book", description: "Every entry, audited" }, ], }, ], }, /* ... 1 more */ ]; /* SuiMegaMenuItem[] */ el.addEventListener("suiPick", (e) => console.log(e.detail)); </script>
import { SuiMegaMenu } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const items = [ { label: "Accounting", items: [ { heading: "Money in", links: [ { label: "Invoices", icon: "fa-regular fa-file-lines", description: "Bill your customers" }, { label: "Payments", icon: "fa-regular fa-circle-check", description: "Receive and deposit" }, ], }, { heading: "Books", links: [ { label: "Chart of accounts", icon: "fa-regular fa-rectangle-list", description: "32 accounts" }, { label: "General ledger", icon: "fa-solid fa-book", description: "Every entry, audited" }, ], }, ], }, /* ... 1 more */ ]; /* SuiMegaMenuItem[] */ return ( <SuiMegaMenu aria-label="Products" 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> </SuiMegaMenu> ); }
<script setup> import { SuiMegaMenu } from "@shineui/vue"; import "@shineui/core/shineui.css"; const items = [ { label: "Accounting", items: [ { heading: "Money in", links: [ { label: "Invoices", icon: "fa-regular fa-file-lines", description: "Bill your customers" }, { label: "Payments", icon: "fa-regular fa-circle-check", description: "Receive and deposit" }, ], }, { heading: "Books", links: [ { label: "Chart of accounts", icon: "fa-regular fa-rectangle-list", description: "32 accounts" }, { label: "General ledger", icon: "fa-solid fa-book", description: "Every entry, audited" }, ], }, ], }, /* ... 1 more */ ]; // SuiMegaMenuItem[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiMegaMenu aria-label="Products" :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></SuiMegaMenu> </template>
import { Component } from "@angular/core"; import { SuiMegaMenu } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiMegaMenu], template: `<sui-mega-menu aria-label="Products" [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-mega-menu>`, }) export class Example { items = [ { label: "Accounting", items: [ { heading: "Money in", links: [ { label: "Invoices", icon: "fa-regular fa-file-lines", description: "Bill your customers" }, { label: "Payments", icon: "fa-regular fa-circle-check", description: "Receive and deposit" }, ], }, { heading: "Books", links: [ { label: "Chart of accounts", icon: "fa-regular fa-rectangle-list", description: "32 accounts" }, { label: "General ledger", icon: "fa-solid fa-book", description: "Every entry, audited" }, ], }, ], }, /* ... 1 more */ ]; // SuiMegaMenuItem[] 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. Inside the panel each column is a role="group" named by its heading, so a screen reader announces the section before its links.

Workspace Dana Whitfield
<sui-mega-menu aria-label="Products"> <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-mega-menu>
// setup(root): properties and events setup(root) { root.querySelector('sui-mega-menu').items = suiteItems(); }

Actions without a panel

A top item with no columns is a plain action. It draws no chevron and emits suiPick on click, which is how a portal nav mixes Products with Pricing and Contact.

Workspace No action yet.
<sui-mega-menu id="mm-mixed"> <b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <span id="mm-mixed-log" slot="end" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">No action yet.</span> </sui-mega-menu>
// setup(root): properties and events setup(root) { const log = root.querySelector('#mm-mixed-log'); const el = root.querySelector('#mm-mixed'); el.items = [accounting(), { label: 'Pricing' }, { label: 'Contact' }]; el.addEventListener('suiPick', (e) => { log.textContent = `Picked ${e.detail.label}.`; }); }

Vertical

orientation="vertical" stacks the items in a rail and opens the panel beside them, sized by its own columns rather than by the bar. The chevron turns to point at where the panel appears.

Workspace
<div style="min-height:320px"> <sui-mega-menu id="mm-vertical" orientation="vertical" style="width:230px" open-index="0"> <b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> </sui-mega-menu> </div>
// setup(root): properties and events setup(root) { root.querySelector('#mm-vertical').items = suiteItems(); }

A panel link or a plain top item takes href and becomes a real anchor with the same keyboard reach. suiPick still fires on the click, then the browser follows.

Workspace Dana Whitfield
<div style="min-height:300px"> <sui-mega-menu id="mm-links" open-index="0"> <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-mega-menu> </div>
// setup(root): properties and events setup(root) { root.querySelector('#mm-links').items = [ { label: 'Accounting', items: [ { heading: 'Money in', links: [ { label: 'Invoices', icon: 'fa-regular fa-file-lines', description: 'Bill your customers', href: '#/invoices' }, { label: 'Payments', icon: 'fa-regular fa-circle-check', description: 'Receive and deposit', href: '#/payments' }, ], }, { heading: 'Learn', links: [{ label: 'Help center', icon: 'fa-regular fa-circle-question', description: 'Guides and answers', href: 'https://example.com/help', target: '_blank' }], }, ], }, { label: 'Pricing', href: '#/pricing' }, { label: 'Contact', href: '#/contact' }, ]; }

Folding into More

In a horizontal bar, top items that do not fit fold into More, starting from the last. More opens a menu, not a panel: a folded panel becomes a row whose submenu lists its links under the column headings, without the descriptions. A vertical bar never folds, and no-fold keeps every item.

Workspace
<div style="max-width:400px;min-height:280px"> <sui-mega-menu id="mm-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-mega-menu> </div>
// setup(root): properties and events setup(root) { root.querySelector('#mm-fold').items = [ ...suiteItems(), { label: 'Banking', items: [{ heading: 'Accounts', links: [{ label: 'Bank feeds', icon: 'fa-solid fa-building-columns', description: 'Two connected banks' }] }], }, { label: 'Pricing' }, ]; }

Open panel

Write open-index to pin a panel. It matches the width of the bar, and while one panel is open, hovering another top item switches to it.

Workspace Dana Whitfield
<div style="min-height:300px"> <sui-mega-menu id="mm-open" open-index="0"> <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-mega-menu> </div>
// setup(root): properties and events setup(root) { root.querySelector('#mm-open').items = suiteItems(); }

Events

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

Workspace Closed.
<div style="min-height:260px"> <sui-mega-menu id="mm-events"> <b slot="brand" style="font-size:14.5px;font-weight:730">Work<span style="color:var(--accent-ink)">space</span></b> <span id="mm-log" slot="end" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Closed.</span> </sui-mega-menu> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#mm-log'); const el = root.querySelector('#mm-events'); const items = suiteItems(); items[0].items[0].links[0].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 panel closes first.
ArrowDown ArrowUpHorizontal: opens the focused item's panel on its first or last link. Vertical: moves to the next or previous top item, wrapping. Inside a panel, moves through the links in reading order, column by column.
ArrowRight ArrowLeftHorizontal: moves to the next or previous top item, wrapping, and takes the open panel with it. Vertical: ArrowRight opens the focused item's panel on its first link, ArrowLeft closes the open panel and returns focus to its top item. In More's menu, the forward arrow on a row with a submenu opens it on its first link, and the back arrow closes it again. Under dir="rtl" the pair swaps, so the arrow that points along the reading direction is always the forward one.
Home EndMoves to the first or last top item. While items are folded, More is the last one. Inside a panel, moves to its first or last link.
Enter SpaceOn a top item, opens its panel on its first link, or closes it, or runs a plain action. Inside a panel, picks the link. On a top item or panel link with href the browser then follows it.
EscapeCloses the panel and returns focus to its top item.
Any letterInside a panel, moves to the next link whose label starts with that letter.

API

<sui-mega-menu>

Properties

PropertyAttributeTypeDefaultDescription
itemsproperty onlySuiMegaMenuItem[][]Top items: { label, icon?, href?, target?, items?: { heading, links: { label, icon?, description?, href?, target? }[] }[] }[]. A panel link or a plain top item with href is a real link with the same keyboard reach. Property, not attribute.
noFoldno-foldbooleanfalseFolds the top items that do not fit into a More item at the end of a horizontal bar. The bar measures its room as it resizes and folds from the end; the brand and end slots stay whole. More opens a menu, not a panel: a folded item with columns becomes a row whose submenu lists its links under the column headings, and a plain item is a row that runs it. A vertical bar never folds. noFold keeps every item in the bar, and a narrow screen cuts the last ones off.
openIndexopen-indexnumber-1Index of the open panel, -1 when closed. Mutable: the element writes it on click, hover and keyboard and reports each change through suiOpen and suiClose. The panel sits under the bar and matches its width; while one is open, hovering another top item switches to it. An open panel or More menu 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.
orientationorientation"horizontal" | "vertical"'horizontal'horizontal is a bar with the panel under it, matching its width. vertical stacks the items in a sidebar and opens the panel beside them, toward the inline end, sized to its own columns. The arrow keys rotate with it: down and up walk the items, the forward arrow opens a panel and the back arrow closes it (right and left under dir="ltr", the reverse under dir="rtl").

Events

EventDetailReactVueDescription
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the element closes the open panel; detail is 'trigger' | 'outside' | 'escape' | 'pick'. Cancelable: preventDefault() keeps it open (and cancels a switch to another panel). It does not bubble.
suiOpennumberonSuiOpen@sui-openFires after the element opens a panel; detail is the top-item index it opened, or items.length for More. It does not bubble.
suiPickSuiMegaMenuLinkonSuiPick@sui-pickFires when a panel link, or a top item without a panel, is picked; detail is the link. 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.
SuiMegaMenuColumn interface
headingstring
linksSuiMegaMenuLink[]
SuiMegaMenuItem interface
labelstring
icon?stringFont Awesome class shown before the label.
items?SuiMegaMenuColumn[]Panel columns. Without them the top item is a plain action, as sui-menubar.
command?() => voidRuns when a top item without a panel is picked.
href?stringRenders a top item without a panel as a real link. Enter and Space follow it, and suiPick and command still run on the click. Ignored on an item with columns.
target?stringLink target for a top item with href, e.g. _blank.
SuiMegaMenuLink interface
labelstring
icon?stringFont Awesome class shown before the label.
description?stringSecond line under the label.
command?() => voidRuns when the link is picked.
href?stringRenders the panel link as a real anchor. Enter and Space follow it, and suiPick and command still run on the click before the browser navigates.
target?stringLink target for a link with href, e.g. _blank. A _blank link gets rel="noopener".

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