Shallow Routing: Navigating smoothly without reloading the page

Shallow routing lets you change the URL without losing the interface state, enabling faster, smoother, and more SEO-friendly experiences. In this article, we dive deep into this concept, its pitfalls, and practical use cases.

Illustration of a user navigating between tabs and floating modals, while the background remains stable to symbolize state conservation.

In an increasingly interactive web, users expect applications to be fast, responsive, and seamless. When switching tabs, filtering a table, or opening a modal, the experience should stay smooth, no loading flashes or lost UI state.

That’s where shallow routing comes in. Still underused, it allows you to change the URL without reloading the page, while preserving the current interface state. A small detail? Not at all. When used properly, it becomes a lever for performance, UX, and even technical SEO.

What is shallow routing?

Shallow routing means modifying the URL in the browser’s address bar without triggering a full page reload or a global data fetch in the application.

Technically, this relies on the browser’s history.pushState() or history.replaceState() methods. These allow you to push a new URL into the browser history without making a network request or resetting the DOM.

This contrasts with classic routing, where every URL change typically results in a full page re-render or a fresh server request.

Behavioral Differences

Aspect Classic Routing Shallow Routing
Page Load Full reload (network request or complete re-render) No reload, interface preserved
URL Modification Yes, via standard navigation Yes, via pushState / replaceState
State Preservation No, state is lost Yes, state is maintained
Performance Less performant Faster, smoother UX

Why use shallow routing?

Here are the key benefits of shallow routing — valuable for developers and tech-savvy marketers alike.

  1. Improved Performance : By avoiding unnecessary network calls and full page refreshes, shallow routing creates near-instant interactions.
  2. Better User Experience : No white flashes, no interrupted scrolls or form resets: the navigation feels smooth and uninterrupted.
  3. Dynamic, Shareable URLs : Shallow routing lets you reflect UI state in the URL (?tab=profile?photo=42...), without triggering a full rerender. Perfect for link sharing, tracking, or tab/modal navigation.
  4. Fine-grained State Management : Instead of storing UI state only in memory, you can sync it with the URL. This is especially useful for filters, sorting, or open modals.

Further Reading:

Common pitfalls to avoid

Shallow routing is powerful, but requires careful handling on the development side.

  1. Proper Handling of the Back and Forward Buttons: Since you manually control the browser history, make sure to listen to popstate events and store a clear state object in pushState. Otherwise, you won't know if you're returning to a modal or a full page.
  2. Manual UI Synchronization: Changing the URL is only half the job — your components need to react to new params. Don’t rely on frameworks or auto-refresh to do it for you.

Full Example: Photo Gallery with Modal

Let’s take a Facebook-style example: a photo gallery. Clicking an image opens it in a modal without leaving the current page. But when you copy and share the URL, someone opening the link sees the photo in full-page mode with all the server-rendered context.
This hybrid pattern is both UX-friendly and SEO-compliant.

HTML: a functional link without JavaScript

<a href="/photo/42" class="photo-link" data-photo-id="42">
  <img src="/img/thumbs/42.jpg" alt="Photo 42" />
</a>

Without JavaScript, the link works as expected, it loads /photo/42 as a full page.
With JavaScript, we intercept the click and show the photo in a modal.

JavaScript: shallow fouting with feal page fetch

document.querySelectorAll('.photo-link').forEach(link => {
  link.addEventListener('click', async (e) => {
    // Block natural link
    e.preventDefault();

    const photoId = link.dataset.photoId;
    const url = `/photo/${photoId}`;

    // Update URL and store modal state
    history.pushState({ modal: true, photoId }, '', url);

    // Fetch the real page via JS (async)
    const response = await fetch(url);

    if (response.ok) {
      const html = await response.text();
      openPhotoModal(html);
    }
  });
});

function openPhotoModal(innerHtml) {
  const modal = document.createElement('div');
  modal.className = 'modal';
  modal.innerHTML = `
    <div class="modal-content">
      ${innerHtml}
      <button id="close-modal">Close</button>
    </div>
  `;
  document.body.appendChild(modal);

  document.getElementById('close-modal').addEventListener('click', () => {
    modal.remove();
    history.back();
  });
}

// Handle Back/Forward buttons
window.addEventListener('popstate', async (event) => {
  const state = event.state || {};

  if (state.modal && state.photoId) {
    const response = await fetch(`/photo/${state.photoId}`);
    const html = await response.text();
    openPhotoModal(html);
  } else {
    const modal = document.querySelector('.modal');
    if (modal) modal.remove();
  }
});

What about the server side?

You can detect server-side whether the request came from a JS fetch() or a direct HTTP navigation by inspecting headers like Accept or Sec-Fetch-Mode. This allows the server to respond with a reduced version of the page (e.g., only the modal content).

If the same URL (/photo/42) can return different content (modal vs. full page), don’t forget to send a Vary header, such as:

Vary: Accept, Sec-Fetch-Mode

This prevents cache layers from serving the wrong version to other users or request types.

Best Practices

Used properly, shallow routing is a great tool for performance, UX, and SEO. It helps you build interfaces that are rich, shareable, and smooth — without breaking navigation or accessibility.
Key Takeaways:

  • Use it for dynamic interactions (modals, filters, tabs...)
  • Keep the URL in sync with UI state
  • Ensure links work without JavaScript
  • Store explicit state to manage back/forward
  • Fetch real pages and adapt server response smartly
  • Don’t misuse it as a replacement for classic routing

Shallow routing isn’t about hacking your way around proper navigation — it’s about enhancing it with more finesse.

Building fluid, shareable, and performant UIs? Let’s talk.

  • SEO
  • Performance
  • UX
  • Best practices
  • Code Quality
Jerome Musialak

Jerome Musialak

CEO @ Enodo

A passionate developer since the age of 11, Jérôme cut his teeth at MinuteBuzz and then as CTO of MeltyGroup, where he managed the technical infrastructure of around thirty high-traffic sites including Virgin Radio, L'Étudiant, ... and the group's media. With this experience and aware of the recurring challenges faced by companies in creating and effectively distributing their content, he founded Enodo (from the Latin "to untie knots") with the mission of simplifying the digital ecosystem. An expert in performance optimization and high-availability architecture, he puts his obsession with technical details to the service of business challenges to build reliable systems that allow everyone to sleep soundly.

On the same subject