Menu Examples
The examples below assume that you have imported the components and set up the theme.
Basic
sv-menu wraps an sv-flyout and turns the flyout's content into a command menu: keyboard navigation, typeahead, and the menu popup role come with it, while the menuitem roles stay yours to declare. Acting on a chosen item is also yours: handle its click and call close(), as every example here does.
<div class="menu-basic"> <sv-menu> <sv-flyout aria-label="Actions menu"> <button type="button" class="trigger" slot="trigger">Actions</button>
<button type="button" class="item" role="menuitem">Edit</button> <button type="button" class="item" role="menuitem">Duplicate</button> <button type="button" class="item" role="menuitem">Delete</button> </sv-flyout> </sv-menu></div>
<script type="application/x-typescript" module> const menu = document.querySelector<Menu>(".menu-basic sv-menu")!;
menu.addEventListener("click", event => { if ((event.target as HTMLElement | null)?.closest('[role="menuitem"]')) { menu.close(); } });</script>
<style> .menu-basic { display: grid; justify-items: center; padding-block: 1em;
sv-flyout::part(content) { min-inline-size: 10rem; }
.trigger { padding: 0.5em 1.25em; font: inherit; color: var(--sv-gray-2); cursor: pointer; background-color: var(--sv-gray-6); border: solid var(--sv-bdr-sz-sm) var(--sv-gray-5); border-radius: var(--sv-bdr-rad-md); }
.item { padding: 0.4em 0.75em; font: inherit; color: var(--sv-gray-2); text-align: start; cursor: pointer; background-color: transparent; border: none; border-radius: var(--sv-bdr-rad-sm);
@media (hover: hover) and (pointer: fine) { &:hover { background-color: var(--sv-gray-5); } } } }</style>Items
Menu items are simply the focusable elements in the flyout's content. The disabled Paste here keeps its place in the list while the arrow keys skip over it.
<div class="menu-items"> <sv-menu> <sv-flyout aria-label="Clipboard menu"> <button type="button" class="trigger" slot="trigger">Clipboard</button>
<button type="button" class="item" role="menuitem">Cut</button> <button type="button" class="item" role="menuitem">Copy</button> <button type="button" class="item" disabled role="menuitem">Paste</button> <button type="button" class="item" role="menuitem">Select all</button> </sv-flyout> </sv-menu></div>
<script type="application/x-typescript" module> const menu = document.querySelector<Menu>(".menu-items sv-menu")!;
menu.addEventListener("click", event => { if ((event.target as HTMLElement | null)?.closest('[role="menuitem"]')) { menu.close(); } });</script>
<style> .menu-items { display: grid; justify-items: center; padding-block: 1em;
sv-flyout::part(content) { min-inline-size: 10rem; }
.trigger { padding: 0.5em 1.25em; font: inherit; color: var(--sv-gray-2); cursor: pointer; background-color: var(--sv-gray-6); border: solid var(--sv-bdr-sz-sm) var(--sv-gray-5); border-radius: var(--sv-bdr-rad-md); }
.item { padding: 0.4em 0.75em; font: inherit; color: var(--sv-gray-2); text-align: start; cursor: pointer; background-color: transparent; border: none; border-radius: var(--sv-bdr-rad-sm);
&[disabled] { color: var(--sv-gray-4); cursor: not-allowed; }
@media (hover: hover) and (pointer: fine) { &:not([disabled]):hover { background-color: var(--sv-gray-5); } } } }</style>Submenus
A nested sv-menu with mode="submenu" turns its flyout into a submenu branch. Hover-opening and the side placement come from the nested flyout's open-on-hover and position-order attributes.
<div class="menu-submenus"> <sv-menu> <sv-flyout aria-label="Actions menu"> <button type="button" class="trigger" slot="trigger">Actions</button>
<button type="button" class="item" role="menuitem">Rename</button> <button type="button" class="item" role="menuitem">Duplicate</button>
<sv-menu mode="submenu"> <sv-flyout aria-label="Share submenu" open-on-hover position-order="horizontal"> <button type="button" class="item" slot="trigger"> <span>Share</span>
<!-- Arrow right --> <svg class="submenu-icon" aria-hidden="true" viewBox="0 -960 960 960" xmlns="http://www.w3.org/2000/svg"> <path d="M400-280v-400l200 200-200 200Z"></path> </svg> </button>
<button type="button" class="item" role="menuitem">Copy link</button> <button type="button" class="item" role="menuitem">Send by email</button> </sv-flyout> </sv-menu>
<sv-menu mode="submenu"> <sv-flyout aria-label="Export submenu" open-on-hover position-order="horizontal"> <button type="button" class="item" slot="trigger"> <span>Export</span>
<!-- Arrow right --> <svg class="submenu-icon" aria-hidden="true" viewBox="0 -960 960 960" xmlns="http://www.w3.org/2000/svg"> <path d="M400-280v-400l200 200-200 200Z"></path> </svg> </button>
<button type="button" class="item" role="menuitem">As PDF</button> <button type="button" class="item" role="menuitem">As PNG</button> </sv-flyout> </sv-menu> </sv-flyout> </sv-menu></div>
<script type="application/x-typescript" module> const menu = document.querySelector<Menu>(".menu-submenus > sv-menu")!;
menu.addEventListener("click", event => { if ((event.target as HTMLElement | null)?.closest('[role="menuitem"]:not([slot="trigger"])')) { menu.close(); } });</script>
<style> .menu-submenus { display: grid; justify-items: center; padding-block: 1em;
sv-flyout::part(content) { min-inline-size: 10rem; }
sv-menu[mode="submenu"] sv-flyout { --sv-flyout-offset: 0; }
.trigger { padding: 0.5em 1.25em; font: inherit; color: var(--sv-gray-2); cursor: pointer; background-color: var(--sv-gray-6); border: solid var(--sv-bdr-sz-sm) var(--sv-gray-5); border-radius: var(--sv-bdr-rad-md); }
.item { display: flex; gap: 0.5em; align-items: center; padding: 0.4em 0.75em; font: inherit; color: var(--sv-gray-2); text-align: start; cursor: pointer; background-color: transparent; border: none; border-radius: var(--sv-bdr-rad-sm);
.submenu-icon { inline-size: 1.2em; block-size: 1.2em; margin-inline-start: auto; fill: currentcolor; }
@media (hover: hover) and (pointer: fine) { &:hover { background-color: var(--sv-gray-5); } } } }</style>Context Menu
mode="context" opens the menu from a contextmenu event inside the trigger, right at the pointer. The trigger stays the keyboard path: focus it and press Shift+F10 or the menu key.
<div class="menu-context"> <sv-menu mode="context"> <sv-flyout aria-label="Page menu"> <button type="button" class="context-target" slot="trigger"> <strong>Right-click here</strong> <span>Or touch and hold, or focus and press Shift+F10.</span> </button>
<button type="button" class="item" role="menuitem">Reload</button> <button type="button" class="item" role="menuitem">Copy</button> <button type="button" class="item" role="menuitem">Select all</button> </sv-flyout> </sv-menu></div>
<script type="application/x-typescript" module> const menu = document.querySelector<Menu>(".menu-context sv-menu")!;
menu.addEventListener("click", event => { if ((event.target as HTMLElement | null)?.closest('[role="menuitem"]')) { menu.close(); } });</script>
<style> .menu-context { display: grid; inline-size: min(100%, 26rem); margin-inline: auto;
sv-flyout { --sv-flyout-offset: 0;
&::part(content) { min-inline-size: 10rem; } }
.context-target { display: grid; gap: 0.5em; place-content: center; min-block-size: 10rem; padding: 1.5em; font: inherit; color: var(--sv-gray-2); text-align: center; touch-action: manipulation; cursor: context-menu; user-select: none; background: var(--sv-gray-7); border: var(--sv-bdr-sz-sm) dashed var(--sv-gray-4); border-radius: var(--sv-bdr-rad-lg); -webkit-touch-callout: none;
&:focus-visible { outline: var(--sv-bdr-sz-md) solid var(--sv-accent); outline-offset: 0.2em; }
span { color: var(--sv-gray-3); } }
.item { padding: 0.4em 0.75em; font: inherit; color: var(--sv-gray-2); text-align: start; cursor: pointer; background-color: transparent; border: none; border-radius: var(--sv-bdr-rad-sm);
@media (hover: hover) and (pointer: fine) { &:hover { background-color: var(--sv-gray-5); } } } }</style>