Introduction

I was recently challenged with a pretty common, and a simple problem of positioning a custom context menu. When you DuckDuckGo how to position a context menu phrase you will be greeted with solutions that either:

The issue with the first solution is that it’s not great in terms of User Experience. The second one is okay but requires measurements and some JavaScript logic. It’s also not responsive to window resizes.

A different approach

After some time spent tinkering I came up with a CSS solution to calculate the context menu position which respects the window boundaries.

TL;DR

Demo

css
#menu {
    --mouse-x: 0;
    --mouse-y: 0;
    display: none;
    position: fixed;
    margin: 0;
    left: 0;
    top: 0;
    /* The following line is responsible for all the magic */
    transform: translateX(min(var(--mouse-x), calc(100vw - 100%))) translateY(min(var(--mouse-y), calc(100vh - 100%)));
}
html
<ul id='menu'>
  <li>Option 1</li>
  <li>Option 2</li>
</ul>
javascript
const menu = document.querySelector('#menu');
// hide the menu
window.addEventListener('click', event => menu.style.display = 'none')
// show the menu when right-clicked
window.addEventListener('contextmenu', event => {
  event.preventDefault()
  menu.style.setProperty('--mouse-x', event.clientX + 'px')
  menu.style.setProperty('--mouse-y', event.clientY + 'px')
  menu.style.display = 'block'
});

Explanation

Let’s focus on the transform property:

#menu {
  transform: translateX(min(var(--mouse-x), calc(100vw - 100%)))
             translateY(min(var(--mouse-y), calc(100vh - 100%)));
}

more specifically on the translateX component, as it’s analogous to translateY.

transform: translateX(min(var(--mouse-x), calc(100vw - 100%)))

Let’s break it down:

  • translateX behaves similarly to left and moves an element along the X-axis.
    • min takes the smaller of two values
      • var(--mouse-x) dereferences a variable, in this case - the mouse position.
      • calc performs a calculation
        • 100vw is the viewport’s width, or simply put - the page width
        • 100% when used inside a translate, yields the current element width

So, the calc(100vw - 100%) directive returns the page width decreased by the width of the context menu.

What we actually get is a value between [0, viewport width - context menu width] describing the x position of the context menu. This will not let the menu be positioned at the very edge of the screen.

Boundaries