Skip to content

Global Context Menu

Use openAtPosition to open a context menu without a trigger. Without a position or trigger, the menu cannot open.

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 component and set up the theme.

Global Context Menu
<div class="global-context-menu-example">
<div class="context-area">
<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 offset="0">
<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="module">
const example = document.querySelector(".global-context-menu-example");
/** @type {HTMLElement} */
const contextArea = example.querySelector(".context-area");
/** @type {Menu} */
const menu = example.querySelector(".global-context-menu");
/** @type {Flyout} */
const flyout = menu.querySelector("sv-flyout");
/** @type {HTMLElement} */
const result = example.querySelector(".context-result");
/** @type {NodeListOf<HTMLButtonElement>} */
const options = menu.querySelectorAll(".context-option");
contextArea.addEventListener("contextmenu", event => {
menu.openAtPosition = { left: event.clientX, top: event.clientY };
});
let scrollLocked = false;
let previousOverflow = "";
const lockScroll = () => {
if (scrollLocked) return;
scrollLocked = true;
previousOverflow = document.documentElement.style.overflow;
document.documentElement.style.overflow = "hidden";
};
const unlockScroll = () => {
if (!scrollLocked) return;
scrollLocked = false;
document.documentElement.style.overflow = previousOverflow;
// When `undefined`, the menu won't open because there is no position nor trigger.
menu.openAtPosition = undefined;
};
flyout.addEventListener("expandstart", lockScroll);
flyout.addEventListener("collapsed", unlockScroll);
window.addEventListener("pagehide", unlockScroll);
for (const option of options) {
option.addEventListener("click", () => {
result.textContent = `${option.textContent} selected.`;
menu.close();
});
}
</script>
69 collapsed lines
<style>
.global-context-menu-example {
display: grid;
gap: 0.75em;
max-inline-size: 40rem;
margin: auto;
sv-flyout::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;
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);
&:focus-visible {
outline: var(--sv-bdr-sz-sm) solid var(--sv-accent);
outline-offset: -0.15em;
}
@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>