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:
- don’t consider boundary conditions or
- use JavaScript to check the conditions and position the menu accordingly.
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 toleft
and moves an element along the X-axis.min
takes the smaller of two valuesvar(--mouse-x)
dereferences a variable, in this case - the mouse position.calc
performs a calculation100vw
is the viewport’s width, or simply put - the page width100%
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.