All articles
Design & UXJuly 26, 2026 6 min read

Modals Are a Routing Problem: Rethinking Dialogs in 2026

Most modal bugs aren't CSS bugs. They're routing bugs, focus bugs, and state bugs pretending to be design decisions. Here's how to actually ship dialogs that behave.

Modals Are a Routing Problem: Rethinking Dialogs in 2026

A user opens a modal on your product page, shares the URL with a teammate, and the teammate lands on... the product page. No modal. That's not a design bug. That's a routing bug, and it's the single most common reason modal UX quietly rots on mature products.

We've spent the last few years auditing dialogs across client codebases, and the pattern is consistent: teams treat modals as visual components when they're actually pieces of application state that deserve URLs, history entries, and the same care you'd give a page.

The four things every modal must handle

Before we get into implementation, here's the checklist we run against every dialog we ship. If any of these fail, the modal is broken — even if it looks pixel-perfect in Figma.

  1. Deep-linkability. Can someone paste a URL and land inside the modal?
  2. Back button. Does browser back close the modal instead of leaving the page?
  3. Focus trap and return. Is focus locked inside, and does it return to the trigger on close?
  4. Escape and scrim behavior. Does Escape close it? Does the scrim? Should it?

Every one of these has a wrong answer that ships to production more often than we'd like to admit. The first two are routing problems. The last two are focus and interaction problems. Most teams solve one category and ignore the other.

Why component-tree modals fail at scale

The default React tutorial teaches you this:

function ProductPage() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button onClick={() => setOpen(true)}>Edit</button>
      {open && <EditModal onClose={() => setOpen(false)} />}
    </>
  );
}

This works for a demo. It breaks the moment your product grows past three screens. The modal's existence is trapped inside a component's local state, which means:

  • The URL doesn't change, so you can't share the state.
  • Refreshing the page loses the modal.
  • Analytics can't distinguish "viewed product" from "opened edit dialog."
  • Back button navigates away from the whole page instead of just closing the dialog.
  • Multiple modals fight for z-index because there's no central authority.

On mobile, that back-button issue is a UX disaster. Android users hit hardware back expecting the modal to dismiss. Instead they get thrown out of the flow entirely.

The mental model shift

Stop thinking about modals as components. Start thinking about them as routes that happen to render on top of another route. Next.js formalized this with parallel routes and intercepting routes, but the idea predates the framework and works anywhere.

A modal is a URL segment. /products/42 shows a product. /products/42/edit shows the same product with an edit dialog layered on top. When you close the dialog, you navigate back to /products/42. The browser handles history. The URL is shareable. The back button just works.

Implementing URL-driven modals

Here's a stripped-down pattern we use with React Router or TanStack Router. The same shape works in Next.js App Router with parallel routes, and in Vue Router with named views.

// routes.tsx
<Route path="/products/:id" element={<ProductPage />}>
  <Route path="edit" element={<EditModal />} />
  <Route path="delete" element={<DeleteConfirm />} />
</Route>

// ProductPage.tsx
function ProductPage() {
  const navigate = useNavigate();
  const { id } = useParams();

  return (
    <>
      <ProductDetails id={id} />
      <button onClick={() => navigate(`/products/${id}/edit`)}>
        Edit
      </button>
      <Outlet />
    </>
  );
}

// EditModal.tsx
function EditModal() {
  const navigate = useNavigate();
  const close = () => navigate('..', { relative: 'path' });
  return <Dialog open onClose={close}>...</Dialog>;
}

Notice what disappeared: no useState, no prop drilling, no event bus to coordinate closing dialogs from three levels deep. The URL is the source of truth. Everything else is a projection.

For Next.js, the equivalent uses intercepting routes with (.)edit folders — same mental model, different syntax. Vendors document this well; we won't reproduce it here.

Focus management, done properly

Routing handles half the problem. The other half is what happens inside the modal once it opens. Screen reader users, keyboard users, and anyone with motor impairment will notice immediately if you get this wrong.

The non-negotiables:

  • Focus moves into the modal on open, ideally to the first interactive element or a labelled heading.
  • Tab and Shift+Tab cycle within the modal only.
  • Escape closes the modal.
  • Focus returns to the triggering element on close.
  • The modal has role="dialog" and aria-modal="true", with aria-labelledby pointing at its heading.

Writing this from scratch is a bad idea in 2026. Use a library that has already dealt with the edge cases: Radix UI, React Aria, or the native <dialog> element with a polyfill for older browsers. The native <dialog> with showModal() gets you focus trapping, Escape handling, and the top-layer stacking context for free — no more z-index wars.

function EditModal() {
  const ref = useRef<HTMLDialogElement>(null);
  const navigate = useNavigate();

  useEffect(() => {
    ref.current?.showModal();
  }, []);

  return (
    <dialog
      ref={ref}
      onClose={() => navigate('..', { relative: 'path' })}
      aria-labelledby="edit-title"
    >
      <h2 id="edit-title">Edit product</h2>
      {/* form */}
    </dialog>
  );
}

The return-focus trap

One subtle bug: if the modal was opened by clicking a button that no longer exists after close (say, you deleted the item), where does focus go? The safe default is a stable landmark — the page heading, the main content container. Don't let focus drop to <body>, because keyboard users then have to Tab from the start of the page to get back to where they were.

Scrim, escape, and the "are you sure" problem

Should clicking the scrim close the modal? It depends, and this is one of those places design tokens can't save you — it's a policy decision per dialog type.

Our rule of thumb:

  • Informational or navigational modals (a product quick view, a filter panel): scrim click and Escape both close. Low cost of accidental dismissal.
  • Forms with unsaved input: scrim click does nothing or triggers a confirm. Escape triggers a confirm. Users lose work when you're too eager to close.
  • Destructive confirmations (delete, cancel subscription): scrim click does nothing. Escape closes because it maps to "cancel" and cancelling is safe.

Getting this wrong turns your polished dialog into a rage-inducing form-loss machine. We've seen conversion drops on checkout upsell modals purely because the scrim was too aggressive. Test with real users, not with your team who knows to be careful.

Motion that respects intent

A modal opening should feel like a layer being placed on top of the page, not a dropdown flying in from the corner. Keep the motion subtle:

  • 150 – 250ms duration for open, slightly faster for close.
  • Opacity fade on the scrim, opacity + small scale (0.96 → 1) or opacity + small translate on the dialog.
  • Respect prefers-reduced-motion and drop transforms entirely — just fade the opacity.
@media (prefers-reduced-motion: reduce) {
  .dialog { animation: fade 120ms ease; transform: none; }
}

Avoid springs and bounces on modals. They read as playful, which is fine for a mascot, wrong for a delete confirmation.

The mobile question

On phones, a centered dialog often shouldn't be a dialog at all. It should be a bottom sheet. The interaction model on mobile is thumb-first, and reaching a centered X button in the top-right corner is genuinely painful on a 6.7-inch screen.

We usually ship the same routed state with two presentations: dialog on md: and up, bottom sheet below. The routing, focus management, and dismissal logic stay identical. Only the visual container swaps. This is one of the biggest quiet wins we've had on mobile conversion — not the sheet itself, but treating mobile dismissal (swipe down, tap outside, back button) as first-class rather than an afterthought.

Where we'd start

If you're inheriting a codebase full of useState-driven modals, don't rewrite them all on Monday. Pick the modal that gets the most traffic — usually login, checkout, or a primary create form — and migrate that one to a URL-driven pattern with proper focus handling. Ship it, watch your analytics for direct-link traffic to the modal URL, and use that data to prioritize the rest.

Audit the four checklist items on your top five dialogs this week. Most teams find at least three failures they can fix in a day. If you want a second set of eyes on your dialog patterns or your broader design system, our design and engineering team does this kind of audit regularly — but honestly, the checklist above will get you 80% of the way there on your own.

#UX#Accessibility#Frontend#Design Systems

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project