Ian Duffy Logo

Building Mobile Menu with the Popover API

15 May 2024

5 mins read

When building websites for mobile, it is common to have middle to large navigations offscreen on mobile devices to reduce what is on screen allowing users to focus on the actual page content.

These are often what we call burger menus, where clicking on a button will slide the menu over the top of the page using JavaScript or CSS checkbox hack.

The Popover API

In recent weeks what is called the popover API has reached all modern browser support, which means we no longer need to use JavaScript libraries, write JavaScript or CSS hacks to get the mobile menu to work.

Using this API means we can reduce the JavaScript we need to display popovers as well it opening on a new layer above all layers removing any issues with z-index in CSS.

The API also brings accessibility benefits such as connecting it to the ESC key on the keyboard, connecting the trigger to the popover, managing focus states and functionality to close when clicking outside of the popover.

Below is a button which opens a basic popover and below is the code that I have used that opens the popover, with the section and button having Tailwind classes but the dialog itself has no custom styles:

This is a basic popover

With it default styles minus the close button. The popover opens in the center of the screen, pressing esc or clicking outside the popover will close it.

<section class="grid place-items-center py-8">
  <button popovertarget="basic-popover" class="button flex">Click Here To Open Popover</button>
  <div popover='auto' id="basic-popover">
      <h2>This is a basic popover</h2>
      <p>With it default styles minus the close button. The popover opens in the center of the screen, pressing esc or clicking outside the popover will close it.</p>
      <button popovertarget="basic-popover" popovertargetaction="hide" class="button danger">Close</button>
  </div>
</section>

How to Build a Popover

To build a popover using the new API we have three new HTML attributes:

  • popover - which is added to the element we want to open. There are two options for this attribute, the default is auto which only allows one popover at a time, clicking outside or pressing esc key will close the popover. The other option is manual which allows multiple popovers and needs to be closed via popover target.
  • popovertarget - this links to the popover element with a matching ID attribute.
  • popovertargetaction - this allows us to set the function of the trigger, this can be set to hide for close buttons, show for menu button and toggle which is the default.

It important to know that the popover is relative to the entire viewport due to it being on a new layer at the top, a new API called Anchor Positioning will allow us to position popovers to items.

Building A Mobile Menu

Using the Popover API we can add some CSS to get this to work like an mobile navigation.

So below is the basic HTML with the popover attributes and the CSS class 'mobileMenuPopover' for the actual mobile menu that will appear on clicking the button.

<button popovertarget='mobile-menu'>
  Menu
</button>

<div popover id='mobile-menu' class='mobileMenuPopover'>
  <button popovertarget='mobile-menu' popovertargetaction='hide'>Close Menu</button>
  <nav>
    <h3>Mobile Menu Appears Here</h3>
  </nav>
</div>

In the below SCSS (CSS with SASS) we are mainly focusing on 'mobileMenuPopover'.

In the first part we set the base styles such as having it just off screen with translate set to '100%' so it slides in from the right if replaced with '-100%' it would slide it from the left side, setting animation transition and the positioning it to the right but removing default inset and setting where I want it without unsetting it I had a few issues.

A new pseudo class called :popover-open has been added, which allows us to style the popover when it is open, here I am using it to set the translate to 0, moving it back on to the screen coming from the right hand side.

And using the ::backdrop pseudo class we can style what is below the popover on the top layer, that covers the rest of the page, clicking on this will close the popover when the popover is set to auto.

.mobileMenuPopover {
	/* Closed state transformed off the screen */
	transform: translateX(100%);
	transition: transform 0.4s, overlay 0.4s ease-out;
	display: block;
	max-width: 400px;
	width: 90vw;
	inset: unset;
	right: 0;
	top: 0;
	height: 100dvh;

	/* Styles while the menu is open */
	&:popover-open {
		transform: translateX(0);
	}

	/* Backdrop that overlays other content */
	&::backdrop {
		background-color: rgba(0, 0, 0, 0.15);
	}
}

Here is the final result of what we built with the above HTML and CSS:

Browser Support

As of May 2024, the Popover API is now supported across all current versions of modern browsers, you can see the full list here via can i use

Polyfill

For websites that need to support older browsers, there is a great polyfill on github by @oddbird. To get this to work on my site inside a React useEffect, I checked if this was supported, if not I would apply the change. I then added some CSS as explained in the polyfill readme.

'use client';
import { useEffect, useState } from "react"
import { apply, isSupported } from '@oddbird/popover-polyfill/fn';

export function PopoverPolyfill() {
    const [isPopoverSupported, setIsPopoverSupported] = useState<boolean>(false);

    useEffect(() => {
        if (isSupported()) {
            setIsPopoverSupported(true)
        } else {
            apply();
            setIsPopoverSupported(false)
        }
    }, [])

    const text = isPopoverSupported ? 'is supported in this browser' : 'is not supported and has been polyfilled.'

    return <div>
        <p>Popover API: {text}</p>
    </div>
}

Typescript

At the current time of writing this, React DOM and TypeScript were not aware of the new attributes for the Popover API, so in a global type folder I added the attributes so I was able to use them like this:

declare module 'react' {
	interface ButtonHTMLAttributes<T> extends React.HTMLAttributes<T> {
		popovertarget?: string;
		popovertargetaction?: string;
	}

	interface HTMLAttributes<T> extends HTMLAttributes<T> {
		popover?: 'manual' | 'auto' | 'none';
	}
}

For the popover attribute I also had to explicit say the manual or auto, otherwise the property would get removed by React.

Summary

The Popover API has completely changed how us as developers should be building traditional mobile menus via JavaScript by letting the browser handle it for us and reducing JavaScript needed.

At the time of writing, popovers are positioned relative to viewport, with the Anchor Positioning API only available in Chromium browsers, however this doesn't mean it doesn't have other use cases such as toast messages using popover JavaScript API rather than JavaScript libraries.