JavaScript Navigation API: Intercept and Control Page Transitions

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.

Leave Comment

Your email address will not be published. Required fields are marked *