Menus Using Popover

An exploration of menus created with popover

A Basic Popover

Here is a basic popover. You can use these anytime you need something that overlays. Think a three dot menu, a login / user menu or settings of any kind.

<button popovertarget="menu-demo-popover">Menu</button>
<div popover id="menu-demo-popover" class="menu">
	<ul class="menu-demo">
		<li><a href="#">Settings</a></li>
		<li><a href="#">My Profile</a></li>
		<li><a href="#">Help</a></li>
		<li><a href="#">Logout</a></li>
	</ul>
</div>

So what’s the difference between popover and dialog? 1. Popover doesn’t inflict inert onto your page. 2. Dialog can be on toplayer or not, Popover is on toplayer.


Popover Actions

You can do a lot with just html here if all you need is basic show and hide. It’s only once we get into positioning & animations does this get a bit trickier.

<button popovertargetaction="show" popovertarget="ampd">Show</button>

<div popover id="ampd" class="menu">
	<button popovertargetaction="hide" popovertarget="ampd">Hide</button>
</div>

Let’s say I wanted everything above, but using only shippable features.

<script>
	let button = document.getElementById('md');
	let popover = document.getElementById('mdp');

	button.addEventListener('click', toggle);

	function update_position() {
		const target_position = button.getBoundingClientRect();
		popover.style.inset = 'unset';
		popover.style.top = target_position.bottom + 'px';
		popover.style.left = target_position.right - target_position.width + 'px';
	}
	const resizeObserver = new ResizeObserver(update_position);
	resizeObserver.observe(popover);
	window.addEventListener('resize', update_position);
	window.addEventListener('scroll', update_position);

	// Animation
	function toggle() {
		const is_opening = !popover.matches(':popover-open');
		const translate = is_opening ? ['0 10px', '0 0'] : ['0 0', '0 10px'];
		const opacity = is_opening ? [0, 1] : [1, 0];

		if (is_opening) popover.showPopover();

		window.requestAnimationFrame(() => {
			let animation = popover.animate(
				{
					translate,
					opacity,
				},
				{
					duration: 300,
					easing: 'ease-in-out',
					fill: 'forwards',
				},
			);
			animation.onfinish = () => {
				if (!is_opening) popover.hidePopover();
			};
		});
	}

	// In Manual mode, you need to trigger keyboard events yourself
	document.addEventListener('keydown', (event) => {
		if (
			event.key === 'Escape' &&
			popover.hasAttribute('popover') &&
			popover.matches(':popover-open')
		) {
			toggle();
		}
	});
</script>

<button id="md" class="menu-button"><img src="/menu.svg" /></button>
<!-- Note: Manual Mode Required -->
<div popover="manual" id="mdp" class="menu">
	<ul class="menu-demo">
		<li><a href="#">Settings</a></li>
		<li><a href="#">My Profile</a></li>
		<li><a href="#">Help</a></li>
		<li><a href="#">Logout</a></li>
	</ul>
</div>

Here’s where things get interesting. This uses a few crazy new APIs, popover, anchor, @starting-style, allow-discrete. Basically a who’s who of unsupported cool stuff. On top of that anchor is still greatly in flux.

Disclaimer - This demo may or may not work. 🤷‍♂️

<button id="menu-anchor" class="menu-button" popovertarget="anchored-menu">
	<img src="/menu.svg" />
</button>

<div popover id="anchored-menu" class="menu">
	<ul class="menu-demo">
		<li><a href="#">Settings</a></li>
		<li><a href="#">My Profile</a></li>
		<li><a href="#">Help</a></li>
		<li><a href="#">Logout</a></li>
	</ul>
</div>

<style>
	#menu-anchor {
		anchor-name: --menu;
	}

	#anchored-menu[popover] {
		transition:
			display 0.3s allow-discrete,
			opacity 0.3s,
			translate 0.3s;
		transition-timing-function: ease-in;
		opacity: 0;
		translate: 0 30px;
		position: absolute;
		inset: unset;
		top: anchor(--menu bottom);
		left: anchor(--menu left);
	}

	#anchored-menu[popover]:popover-open {
		opacity: 1;
		translate: 0 0;
		transition-timing-function: ease-out;
	}

	@starting-style {
		#anchored-menu[popover]:popover-open {
			opacity: 0;
			translate: 0 30px;
		}
	}
</style>

Side note - Anchor Positioning

Anchor positioning is a fix for elements placed in the top-layer where relative context doesn’t exist. This is how you pin a menu to a location. It’s great, but not only does support suck, the API itself is not agreed upon and still in flux. See: Anchor Position Issues

Can I use anchor positioning? - ❔ Big No 48% support

But wait?! Is there a Polyfill?

There is, Anchor Polyfill but, it’s not currently current to the spec, and the spec isn’t final, so hold off for now.

Further reading