Adventures in Web Modals

Jon Mulligan
Eureka Engineering
6 min readDec 8, 2020

--

Hi there! I’m Jon Mulligan, a Front End Web Engineer working on the Japanese version of Pairs.

This article is day 8 of the Eureka Advent Calendar 2020. For day 7, Spice wrote about using Svelte components in a UI Framework such as React or Vue [Japanese language].

In application design, splitting large and complex interfaces into pages without the jarring context change can be a challenge. Modal views help to solve these problems by presenting additional content in situ as the user interacts with the page. But from an implementation perspective, they introduce a number of challenges.

In this article I’ll be diving into some of the common challenges and pitfalls of modal content, and how we solved them at Eureka.

There’s a code portion a little later on which covers a very rudimentary implementation using React Router but in theory, these concepts can be applied to any library/framework.

What is a modal?

I’m sure by now, you’re all familiar with modals. Even if you don’t know of them by name, you interact with them every day. They can be as simple as an alert/dialog box asking you to save your work before closing, or as complex as a setup wizard when starting an app for the first time.

Put simply,
A modal is a child view which covers the page beneath it, requiring the user’s attention and interaction before returning control to the parent page.

Simple alerts, lightboxes and dialog boxes are all contextual to an action performed on the parent page. We’ll be skipping these examples and focussing on an increasingly common pattern — Navigable modals.

Many modern web and mobile apps call for unique items, or even entire pages to be shown in a window which covers the parent page or a sheet that slides in from the side of the screen.

Left: A Google Calendar event is shown in a modal. Right: A shopping cart is displayed as an overlay panel.

In the desktop version of Pairs, we show user profile pages in a modal to allow users to quickly jump between profiles and listing pages without losing context.

A design sample of the Pairs web app.

Modal navigation: An idea

A web page is usually a unique piece of content, therefore it has a URL. A way to access the page directly. Visiting the page adds it to your browser’s history which we can navigate using the back and forward buttons. To put an entire page in a modal, we first have to answer a few questions.

Most of the modals you see on the web don’t have URLs — and in many cases, this is fine. But how many times have you used an e-commerce site where a product was shown in a modal, or the help screen was shown in a popup? Without URLs, these pages wouldn’t be shareable. Refreshing the page, or clicking the browser’s back/forward buttons would take us to a completely different location to what we were expecting.
This behaviour creates an inherently different user experience to the rest of the web, and users will lose confidence in your application.

Single-Page Apps (SPA) solve the navigation problem using the History API. Without actually navigating to a new page, we can insert a new history item and preserve natural browsing behaviours like refresh and back/forward navigation. By utilising the History API it’s entirely possible to make modals navigable just like any other page.

The difficult part

By choosing to render something in a modal, we’re technically rendering two unique pieces of content. The modal content itself, and the parent page beneath.

Let’s say we show a piece of content in a modal and the user refreshes their browser. Upon reloading, how would we know what the parent page was? Our only clue is the current URL which directs us to the modal content! This is where we utilise another feature of the History API: state.

When navigating between pages your browser’s history doesn’t just store a list of previously visited URLs, it also has the ability to store an arbitrary JavaScript object with each history item. When we (or our routing library) use history.pushState() or it’s cousin history.replaceState(), not only do we pass a new URL, but we can also pass a state object. After navigating around the app or even refreshing the browser, returning to that item in history gives us access to that same JavaScript object we saved earlier.

If we were to store information about the parent page in state, we can read it back after navigation/refresh, to determine what content to render on the parent page. With this, we have a navigable modal!

Twitter is an example which gets this right.
If you click on a photo and refresh your browser window, you’ll notice that the parent page is still the same prior to refreshing. Closing the photo takes you back exactly where you were.
Well done Twitter. 10 points to you. 👏

Shut up and show me some code

The following (vastly simplified) example is inspired by our implementation in the Pairs web project. Modal routes are mounted just like any other. The example uses React and React router, but the core concepts can be applied to any framework.

Firstly, we define our routes. Both regularRoutes and modalRoutes share the same basic config.

Our route definitions are simple objects, defining a path and a component.

Next, we render all of our routes in two separate <Switch> components. These switches are passed a location prop. This tells the switch which path to match. Without this, it would default to the current path. The parentLocation and modalLocation values are determined by the getLocations() hook, described next.

Our App.tsx mounts both the regularRoutes and the modalRoutes. A getLocations hook determines which route to render in a modal, and which is the parent page.

Our getLocations() hook looks a little like this. If the history state contains a backdropLocation, we can assume it is the parent (background) location and return it as parentLocation . The modalLocation should be the current URL.
If backdropLocation is undefined, we can assume that the current URL is not a modal, and return is as parentLocation.

Our getLocations() hook determines the modal and parent page by reading the backdropLocation which is stored in state.

Finally, we need a way of setting the backdropLocation in our state. This is achieved by using a custom <ModalLink> component which extends the React Router <Link> component. This can be used on any of our app’s pages, and will open a modal with the correct parent page beneath.

The <ModalLink> component appends our backdropLocation object to the history’s state. Notice how we don’t replace the backdropLocation if one is already set. This is a safety mechanism to stop a modal being rendered as a parent page.

The missing piece

You’ll notice that our <ModalLink> component has a safety mechanism to stop a modal being rendered as a parent page to another modal. backdropLocation: backdropLocation || currentLocation
This is just one piece of the puzzle.

If the user opens a modal URL in a new tab or sends it to a friend, a backdropLocation will not be included in the history state and our code will assume it’s a regular page. Therefore we must add some sort of defaultBackdrop configuration to our modalRoutes and redirect if the current URL is a modal.
A specialised getModalRouteRedirector() hook returns a <Redirect> component in this situation which — unless falsey — should override the default render of App.tsx. This should re-write the history state once, after which the App can render as normal.

And there we have it! A massive chunk of code to monkey-patch support for navigable modals.

The real fun starts here

If you’re only showing one simple page in a modal, the above solution is great. It’s simple and isn’t too invasive. But it does have limitations.

If we wanted behaviour even more like a native app — a design that calls for navigation within modals or navigation to other modals — we have to think of a more drastic solution. Our main limitation of the web is the linearity of its history model. It’s essentially a flat array of items.
We’re inventing concepts that the web’s never seen; such as creating a “branched” context, and “reverting” to a previous state on close.

By recording a tree of context branches, and appending it to the state object of each history item, we could theoretically implement such an experience. But my wacky ideas will have to wait for another day.

Conclusion

If your designer asks you to put a page in a modal; pay them to change their mind. Unless you're some sort of lunatic or tech masochist…

I’m obviously kidding.
Modals can be a great way to increase screen real-estate and avoid jarring context switches. By making them navigable, not only do we make them shareable, but we also preserve natural browsing behavior. Whilst we face some challenges along the way, it’s still entirely possible, and pretty fun if you ask me!

--

--

Jon Mulligan
Eureka Engineering

I’m a Frontend Web Engineer @Eureka, Inc. working on Pairs Japan