Modals and the history API

The search for the ultimate user experience

Update! Modals should be built using the <dialog> element, which has not been implemented in this demo. Please refer to the MDN <dialog> docs for more information.

Overview

This website is a simple demonstration of how to use the history API to improve the user experience of modals. It does so without changing the URL of the page, so that the page can't be bookmarked with the modal open (if the content is such that the user should be able to bookmark it, then perhaps it should be an entirely separate page).

The impetus for this project was to try to improve the user experience of modals on mobile devices, specifically Android devices, where the OS provides a very prominent back button that is generally always present on screen. In this case a user may open a modal that fills a large portion of the screen and then hit the back button expecting the modal to close, but instead, the user is often taken back to the page they were on before. Whether or not this technique should be used is dependent upon the specific application being built.

Goals:

Go to the demo page to see this in action. Or keep reading to learn about the implementation.

How to do it

High level:

  1. Use history.pushState() to create a history entry with a state value that indicates that the modal should be open.
  2. On page load and navigation, check the history's state value to determine whether or not to render the modal.

Creating the history entry

Here's what pushState() looks like:

history.pushState(state, title, url)

And here's what those parameters mean:

state An object that gets saved with the history entry.
title This is deprecated so don't use it.
url This is the URL to set in the browser's URL bar. In this demo we're not using it because the modal does not represent an entirely different set of content, so it should not have its own unique URL. Note that if you do use this it does not actually cause a page navigation, it just changes what the user sees in the URL bar.

Here's what we'll use:

history.pushState('modal-open', null, null);

Checking the history state

To access the history state we can use history.state, but the trick is knowing when to check it. At a high level there are two types of events where we need to check the history state:

  1. When the page is loaded.
  2. When history is traversed

Here are the various ways this can happen:

User action What is triggered
Initial page load Global scope
Refresh page Global scope
Fwd/back that is server rendered Either bfcache* or global scope
Fwd/back that is client rendered (i.e. navigating history that has been added using JavaScript) popstate

* When bfcache is used, we don't have to do anything to get the modal to display again, that's handled by bfcache itself. So we can ignore that.

The end result is that we will check the history state in the global scope and on popstate.

The code

function handleModal() {
  if (history.state === 'modal-open') {
    // show modal
  } else {
    // hide modal
  }
};

handleModal();

window.addEventListener('popstate', function() {
  handleModal();
});

openModalButton.addEventListener('click', function() {
  history.pushState('modal-open', null, null);
  handleModal();
});

Note: When clicking the button to open the modal, instead of opening the modal and then pushing to history, we're pushing to history and then using the same logic we use everywhere else to see if a modal should be displayed, and allowing that code to open the modal. The goal here was to reduce duplication of code.

There's a bit more to it than that, but this covers the basics. To see the full details, check out the JavaScript file.

Try it out

Notes on the implementation

Additional information

Additional resources