The Navigation API (window.navigation) is the modern replacement for intercepting URL changes in single-page applications. It fires for all navigations — link clicks, form submissions, back/forward — making it far more complete than the old history.pushState + popstate pattern used in many WordPress JavaScript plugins.
Problem: A WordPress theme with client-side routing — tabbed content, multi-step forms, paginated results loaded via Fetch — changes the URL with history.pushState() but the browser's back button and reload do not integrate cleanly with the server-side routing.
Solution: Use the Navigation API (window.navigation) — intercept navigations with navigation.addEventListener('navigate'), call e.intercept({ handler }) to take control of the transition, and update the DOM within the handler. The browser's back/forward buttons and document.title updates work automatically.
The examples below intercept navigations to add a loading indicator, prevent navigation away from a dirty form, and implement a simple client-side router for a WordPress admin SPA.
// 1. Intercept all navigations and add a progress indicator
if ( 'navigation' in window ) {
navigation.addEventListener( 'navigate', ( event ) => {
// event.navigationType: 'push' | 'replace' | 'reload' | 'traverse'
// event.destination.url: the target URL
// event.canIntercept: false for cross-origin navigations
if ( ! event.canIntercept ) return; // let the browser handle cross-origin
const targetUrl = new URL( event.destination.url );
// Only intercept navigations within our admin page
if ( ! targetUrl.pathname.startsWith( '/wp-admin/admin.php' ) ) return;
event.intercept( {
async handler() {
// Show loading state
document.body.classList.add( 'is-navigating' );
try {
const html = await fetch( event.destination.url ).then( r => r.text() );
updatePageContent( html ); // your SPA render function
} finally {
document.body.classList.remove( 'is-navigating' );
}
},
} );
} );
}
Prevent navigation away from unsaved changes and programmatically navigate:
// 2. Block navigation when a form has unsaved changes
let formIsDirty = false;
document.querySelector( '#my-settings-form' )?.addEventListener( 'input', () => {
formIsDirty = true;
} );
if ( 'navigation' in window ) {
navigation.addEventListener( 'navigate', ( event ) => {
if ( ! formIsDirty ) return;
event.intercept( {
handler() {
const confirmed = window.confirm( 'You have unsaved changes. Leave anyway?' );
if ( ! confirmed ) {
// Returning a rejected promise aborts the navigation
return Promise.reject( new Error( 'Navigation cancelled by user' ) );
}
formIsDirty = false;
return Promise.resolve();
},
} );
} );
}
// 3. Programmatic navigation with state (replaces history.pushState)
async function navigateTo( path, state = {} ) {
if ( 'navigation' in window ) {
await navigation.navigate( path, { state, history: 'push' } ).finished;
} else {
// Fallback for browsers without Navigation API
history.pushState( state, '', path );
window.dispatchEvent( new PopStateEvent( 'popstate', { state } ) );
}
}
// Read state on any navigation:
navigation.addEventListener( 'navigate', ( e ) => {
const state = e.destination.getState();
if ( state?.tab ) switchTab( state.tab );
} );
NOTE: The Navigation API is supported in Chrome 102+ and Edge 102+. Firefox and Safari do not yet support it (as of 2024). Use the 'navigation' in window check and fall back to history.pushState + popstate for unsupported browsers. The navigation-api-polyfill package provides a spec-compliant fallback.