EdgeCases Logo
Feb 2026
Browser APIs
Surface
6 min read

Navigation API: The Modern History API Replacement

The modern History API replacement that centralizes SPA routing into a single navigate event

navigation-api
spa
routing
history-api
browser-apis
javascript

Every SPA you've built has the same hack: intercept link clicks with e.preventDefault(), call history.pushState(), then update the DOM. The Navigation API replaces this entire pattern with a single, centralized navigate event that handles links, form submissions, back/forward buttons, and programmatic navigation—all in one place.

The History API Problem

// ❌ Classic SPA routing: fragmented and incomplete
function updatePage(event) {
  event.preventDefault();
  window.history.pushState(null, '', event.target.href);
  renderRoute(event.target.href);
}

// Intercept all links (but miss dynamically added ones)
document.querySelectorAll('a[href]').forEach(link => {
  link.addEventListener('click', updatePage);
});

// Handle back/forward separately
window.addEventListener('popstate', () => {
  renderRoute(location.href);
});

// Forms? Image maps? Programmatic navigation? More listeners...

This approach fails in subtle ways: dynamically added links need re-binding, form submissions require separate handling, and popstate doesn't fire on pushState calls. The History API was designed for page-by-page navigation, not SPAs.

The Navigation API: One Event

// ✅ Modern SPA routing: centralized and complete
navigation.addEventListener('navigate', (event) => {
  if (!event.canIntercept || event.hashChange || event.downloadRequest) {
    return; // Let the browser handle these
  }

  const url = new URL(event.destination.url);
  
  if (url.pathname.startsWith('/app/')) {
    event.intercept({
      async handler() {
        await renderRoute(url.pathname);
      }
    });
  }
});

The navigate event fires for all navigation types: link clicks, form submissions, navigation.navigate() calls, and even back/forward buttons (with restrictions). One listener handles your entire routing logic.

Key NavigateEvent Properties

navigation.addEventListener('navigate', (event) => {
  // Can you intercept? (false for cross-origin)
  event.canIntercept;
  
  // Destination URL
  event.destination.url;
  
  // Navigation type: 'push', 'replace', 'reload', 'traverse'
  event.navigationType;
  
  // Is this a #hash change only?
  event.hashChange;
  
  // Is this a download?
  event.downloadRequest;
  
  // Is this a form submission? (FormData or null)
  event.formData;
  
  // Abort signal for cancellation
  event.signal;
});

The formData property is particularly useful—you can intercept form POSTs and handle them client-side without a full page reload.

Handling Async Navigation

navigation.addEventListener('navigate', (event) => {
  if (!event.canIntercept) return;
  
  const url = new URL(event.destination.url);
  
  event.intercept({
    async handler() {
      // Show loading state immediately
      showLoadingSpinner();
      
      // Fetch data with abort signal
      const response = await fetch(`/api${url.pathname}`, {
        signal: event.signal
      });
      
      const data = await response.json();
      renderPage(data);
    }
  });
});

// Success/failure events
navigation.addEventListener('navigatesuccess', () => {
  hideLoadingSpinner();
});

navigation.addEventListener('navigateerror', (event) => {
  showError(event.error);
});

The event.signal is an AbortSignal—if the user navigates again mid-request, pending fetches are automatically cancelled. No more race conditions.

Scroll and Focus Handling

event.intercept({
  // Default: scroll to fragment or top after handler resolves
  scroll: 'after-transition', // or 'manual'
  
  // Default: focus first autofocus element or body
  focusReset: 'after-transition', // or 'manual'
  
  async handler() {
    await loadContent();
    
    // Manual scroll if needed
    event.scroll();
  }
});

The browser handles scroll restoration for back/forward navigation automatically—including restoring the exact scroll position, not just scrolling to top.

Programmatic Navigation

// Push new entry (triggers navigate event)
await navigation.navigate('/dashboard');

// Replace current entry
await navigation.navigate('/settings', { history: 'replace' });

// With state
await navigation.navigate('/profile', { 
  state: { tab: 'settings' } 
});

// Back/forward
await navigation.back();
await navigation.forward();
await navigation.traverseTo(entry.key);

// Current entry and history
const current = navigation.currentEntry;
const entries = navigation.entries();

Unlike history.pushState(), navigation.navigate() returns a Promise that resolves when the navigation completes—including any async work in your intercept handler.

Browser Support

The Navigation API is supported in Chrome 102+ and Edge 102+. Safari and Firefox are still implementing it. For production SPAs, consider feature detection with a History API fallback:

if ('navigation' in window) {
  setupNavigationAPI();
} else {
  setupHistoryAPIFallback();
}

Key Takeaways

  • One navigate event replaces fragmented click/popstate handling
  • event.signal provides built-in request cancellation
  • Scroll/focus restoration is automatic for traversals
  • navigation.navigate() returns a Promise—await your navigations
  • Feature-detect and provide a History API fallback for now

Advertisement

Related Insights

Explore related edge cases and patterns

Browser APIs
Surface
Intersection Observer rootMargin: The Scroll Container Trap
6 min
React
Deep
React Suspense Error Boundaries: Recovery Patterns
7 min

Advertisement