Skip to content

Global Context Menu

A context menu for a whole surface, with no trigger in the flyout at all. A contextmenu listener on the area assigns openAtPosition, and the menu opens at that fixed viewport coordinate. Scrolling is locked while the menu is open, because a fixed coordinate does not follow the page. Closing clears openAtPosition again: with no position and no trigger, the menu cannot open, so a right-click outside the area does nothing.

Context menu area Right-click or touch and hold anywhere in this rectangle.

Choose an action from the context menu.

Code

The following example assumes that you have imported the components and set up the theme.

Global Context Menu
<div class="global-context-menu-example">
16 collapsed lines
<div class="context-area" tabindex="-1">
<strong>Context menu area</strong>
<span>Right-click or touch and hold anywhere in this rectangle.</span>
</div>
<sv-menu class="global-context-menu" mode="context">
<sv-flyout aria-label="Page actions menu">
<button type="button" class="context-option" role="menuitem">Refresh</button>
<button type="button" class="context-option" role="menuitem">Paste</button>
<button type="button" class="context-option" role="menuitem">Select all</button>
<div class="divider" role="separator"></div>
<button type="button" class="context-option" role="menuitem">Open developer tools</button>
</sv-flyout>
</sv-menu>
<p class="context-result" aria-live="polite">Choose an action from the context menu.</p>
</div>
<script type="application/x-typescript" module>
40 collapsed lines
const example = document.querySelector(".global-context-menu-example")!;
const contextArea = example.querySelector<HTMLElement>(".context-area")!;
const menu = example.querySelector<Menu>(".global-context-menu")!;
const flyout = menu.querySelector<Flyout>("sv-flyout")!;
const result = example.querySelector<HTMLElement>(".context-result")!;
const options = menu.querySelectorAll<HTMLButtonElement>(".context-option");
contextArea.addEventListener("contextmenu", event => {
menu.openAtPosition = { left: event.clientX, top: event.clientY };
});
const lockScroll = () => {
document.documentElement.style.setProperty("overflow", "hidden");
};
const unlockScroll = () => {
document.documentElement.style.removeProperty("overflow");
menu.openAtPosition = undefined;
};
flyout.addEventListener("openedchange", () => {
if (flyout.opened) {
lockScroll();
}
});
flyout.addEventListener("transitionfinish", () => {
if (!flyout.opened) {
unlockScroll();
}
});
window.addEventListener("pagehide", unlockScroll);
for (const option of options) {
option.addEventListener("click", () => {
result.textContent = `${option.textContent} selected.`;
menu.close();
});
}
</script>
<style>
67 collapsed lines
.global-context-menu-example {
display: grid;
gap: 0.75em;
max-inline-size: 40rem;
margin: auto;
sv-flyout {
--sv-flyout-offset: 0;
&::part(content) {
min-block-size: 100px;
}
}
.context-area {
display: grid;
gap: 0.5em;
place-content: center;
min-block-size: 18rem;
padding: 1.5em;
color: var(--sv-gray-2);
text-align: center;
touch-action: manipulation;
cursor: context-menu;
user-select: none;
outline: none;
background:
linear-gradient(var(--sv-gray-7), var(--sv-gray-7)) padding-box,
repeating-linear-gradient(135deg, var(--sv-gray-5) 0 1px, transparent 1px 10px) border-box;
border: var(--sv-bdr-sz-sm) solid transparent;
border-radius: var(--sv-bdr-rad-lg);
-webkit-touch-callout: none;
span {
color: var(--sv-gray-3);
}
}
.context-option {
padding: 0.4em 0.75em;
font: inherit;
color: var(--sv-gray-2);
text-align: start;
cursor: pointer;
background: transparent;
border: 0;
border-radius: var(--sv-bdr-rad-sm);
@media (hover: hover) and (pointer: fine) {
&:hover {
background: var(--sv-gray-5);
}
}
}
.divider {
block-size: 1px;
background: var(--sv-gray-5);
}
.context-result {
min-block-size: 1.5em;
margin: 0;
color: var(--sv-gray-3);
text-align: center;
}
}
</style>