Route-based Modals with Remiximage
All Stories

Route-based Modals with Remix

Modals are a ubiquitous UX pattern. Traditionally, modals are used to simply focus user attention onto some piece of information (i.e. alerts, confirmation dialogs, light boxes, etc.)

But recently, modals are often being used as small, single action screens that are accessible throughout our applications. For example, the "compose new tweet" feature on Twitter displays as a modal:

New tweet modal

To provide the best user experience, these modals are usually route-based so that they work within the context of the browser's history. This means hitting the back/forward buttons will close or reopen the modal, and users can share or bookmark the link and be taken back to that same screen with the modal opened.

So, let's see how we can build this type of behavior in Remix. Quick note that all of the source code for this article is available here.

Let's get started...

To make this work, we're going to utilize layout routes in Remix. Layout routes are exactly what they sound like - routes that wrap and define a layout for their children. This concept of nested routing is incredibly powerful as it allows us to match sections of pages in our application to parts of the URL.

We can easily setup a layout route in our project by creating a file and a folder with a matching name. In our case, we'll create a tweets.tsx and a tweets folder at the root of our routes folder. Something like this:

routes
| index.tsx
| tweets.tsx
| tweets
	| index.tsx
root.tsx

Now, inside of our tweets.tsx, we'll add some markup and an <Outlet />. This <Outlet /> is where we are telling Remix to render the children of this layout.

// tweets.tsx
import { Outlet } from "@remix-run/react";

export default function () {
  return (
    <div>
      Layout
      <div style={{ color: "blue", border: "1px solid blue" }}>
        <Outlet />
      </div>
    </div>
  );
}

Then, we'll add some basic content to our /tweets/index.tsx route so we can see how the outlet works.

// /tweets/index.tsx
export default function () {
  return <div>Child</div>;
}

Now, when we navigate to /tweets, we see this:

Layout route example showing a child route.

Notice how we see both strings displayed 🙌.

That's cool!

With this information, we might be tempted to just make our modals child routes of our layout route in Remix. For example, having a tweets/new route render our modal component with a folder structure like this.

routes
| index.tsx
| tweets.tsx <-- layout route
| tweets
	| index.tsx
	| new.tsx <-- modal
root.tsx

However, this setup won't work for two reasons. The first is that we are changing the path of our URL from /tweets to /tweets/new. That means our content in index.tsx that appear on /tweets inside of the layout Outlet will no longer be rendered. Our modal will appear above an empty layout page.

The second is that our modal will only display over the /tweets page. In an ideal setup, the NewTweetModal is accessible by our users from any page.

Which means, we need a way of appending dynamic state information to our routes. In other words, it's time to employ an old friend of the web...

The humble ?search parameter.

Search, or query, parameters are ways of extending a basic URL to define what content should be displayed on a given page. They're commonly seen on search or filtering pages. However, in our case, we're going to use a ?compose_tweet=true param to tell Remix to show our modal.

Inside of our layout route, tweets.tsx , let's add the following code:

// tweets.tsx
export default function () {
  const [searchParams] = useSearchParams();
  return (
    <>
      <div>
        Layout
        <div style={{ color: "blue", border: "1px solid blue" }}>
          <Outlet />
        </div>
      </div>
      {searchParams.get("compose_tweet") && <NewTweetModal />}
    </>
  );
}

Notice how we're checking whether or not the search params returned from the useSearchParams hook contains compose_tweet. And, if it does, we render our <NewTweetModal />.

Now, if we go to /tweets, we'll see the page we had before. But, if we go to /tweets?compose_tweet=true:

Initial query param modal example.

Yay! Now, we can add as many routes as we want inside of our tweets folder and if we add our search parameter, our modal will show! But...

Can we do better?

We did say we wanted our modals to be accessible from any page - not just pages that fall after /tweets . Well, luckily, this is an easy refactor. See, the root.tsx file in Remix is actually a root layout route. Notice the Outlet in the code below

export default function App() {
	return (
		<html lang="en" className="h-full bg-sand">
			<head>
				<Meta />
				<Links />
			</head>
			<body className="h-full">
				<Outlet />
				<ScrollRestoration />
				<Scripts />
				<LiveReload />
			</body>
		</html>
	);
}

So, let's add our modal watching code to root.tsx:

export default function App() {
	const [searchParams] = useSearchParams();
	return (
		<html lang="en" className="h-full bg-sand">
			<head>
				<Meta />
				<Links />
			</head>
			<body className="h-full">
				<Outlet />
				<ScrollRestoration />
				<Scripts />
				<LiveReload />
				{searchParams.get("compose_tweet") && <NewTweetModal />}
			</body>
		</html>
	);
}

And now, if we go to the root route in our browser /:

Initial index page

And then we append our search param /?compose_tweet=true:

Modal showing over the index page.

And our modal still appears on /tweets?compose_tweet=true

Modal showing over the tweets page

🎉🥳🎉 Amazing!

We just used Remix to create a route-based modal that can appear above any page in our application.

If you're interested in a follow-up article where I show you how to hook up this modal's Form to a route action so that it actually creates a tweet and refreshes the feed, tweet at me. We might even make it available from a keyboard shortcut 😉.

Best,
Drew Lyton
Drew Lyton
Monday, June 20, 2022

I'm a software engineer, ex-founder and writer who cares deeply about creating a better relationship between technology and society. I'm currently writing a book about early stage entrepreneurship.

LET'S KEEP THE CONVERSATION GOING

If this article has been valuable and you'd like to be notified the next time I post, subscribe to my newsletter!

Each week, I share my latest post along with three other works from people smarter than me on whatever topic is top of mind. Of course, you can unsubscribe at anytime.