Skip to content

Page Transitions in MPAs — Navigation Patterns and Full-Page Reloads

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Page Transitions in MPAs. We cover key concepts, practical examples, and best practices to help you master this topic.

MPA page transitions involve full browser navigation with loading indicators, focus management, scroll restoration, and progressive enhancement techniques for smoother user experience.

What You'll Learn

By the end of this tutorial, you will understand how page navigation works in MPAs, how to manage loading states, preserve scroll position, handle focus for Accessibility, and progressively enhance navigation for a smoother feel.

Why It Matters

Full-page reloads are the defining characteristic of MPAs. While they provide clear visual feedback that navigation occurred, they also cause a flash of white and lose scroll position. Understanding how to manage and enhance these transitions is key to building MPAs that feel polished.

Real-World Use

The GOV.UK website uses MPAs with progressive enhancement. Basic navigation works with full page loads. JavaScript enhances the experience by adding loading indicators, preserving scroll position on back navigation, and managing focus for screen readers.

MPA Navigation Lifecycle
    ┌──────────────────────────────────────────────────────────┐
    │              MPA Page Transition Sequence                 │
    ├──────────────────────────────────────────────────────────┤
    │                                                          │
    │  1. User clicks link                                     │
    │     │                                                    │
    │  2. Browser cancels current page rendering               │
    │     │                                                    │
    │  3. Page goes blank (or shows cached version)            │
    │     │                                                    │
    │  4. Browser displays loading spinner (tab indicator)     │
    │     │                                                    │
    │  5. Server processes request, returns HTML               │
    │     │                                                    │
    │  6. Browser receives response, starts parsing            │
    │     │                                                    │
    │  7. Browser renders new page (FCP, LCP)                  │
    │     │                                                    │
    │  8. Scroll position resets to top                        │
    │     │                                                    │
    │  9. Focus moves to the new page title                    │
    │     │                                                    │
    │  10. Page is fully interactive (TTI)                     │
    │                                                          │
    └──────────────────────────────────────────────────────────┘

Think of MPA navigation like flipping pages in a physical book. Each page turn clears the previous page and shows new content. You lose your place (scroll position), and your eyes need to find where to start reading again (focus management). Progressive enhancement adds bookmarks and guides to make this Process smoother.

Loading Indicators

// Add a loading indicator for navigation
(function() {
    let navigationStart = null;
    const loadingBar = document.createElement('div');
    loadingBar.id = 'loading-bar';
    loadingBar.style.cssText = `
        position: fixed; top: 0; left: 0; height: 3px;
        background: #0066cc; z-index: 9999;
        transition: width 0.3s ease;
        width: 0;
    `;
    document.body.appendChild(loadingBar);

    // Show loading bar on link clicks
    document.addEventListener('click', (e) => {
        const link = e.target.closest('a');
        if (!link || link.target === '_blank') return;
        if (link.hostname !== window.location.hostname) return;

        navigationStart = Date.now();
        loadingBar.style.width = '30%';
    });

    // Hide loading bar on page load
    window.addEventListener('pageshow', () => {
        loadingBar.style.width = '100%';
        const elapsed = Date.now() - (navigationStart || Date.now());
        setTimeout(() => {
            loadingBar.style.width = '0';
        }, Math.max(200, 500 - elapsed));
    });

    // Handle back/forward cache (bfcache)
    window.addEventListener('pagehide', () => {
        loadingBar.style.width = '0';
    });
})();

Scroll Position Restoration

// Preserve and restore scroll position
(function() {
    const scrollPositions = {};

    // Save scroll position before navigation
    document.addEventListener('click', (e) => {
        const link = e.target.closest('a');
        if (!link) return;
        if (link.hostname !== window.location.hostname) return;

        const key = window.location.pathname + window.location.search;
        scrollPositions[key] = {
            x: window.scrollX,
            y: window.scrollY
        };

        // Store in sessionStorage for persistence across navigations
        try {
            sessionStorage.setItem('scrollPositions',
                JSON.stringify(scrollPositions));
        } catch (e) {
            // sessionStorage might be full
        }
    });

    // Restore scroll position on page load
    window.addEventListener('load', () => {
        // First try to restore from History API
        if (window.history.state && window.history.state.scrollY) {
            window.scrollTo(window.history.state.scrollX, window.history.state.scrollY);
            return;
        }

        // Fall back to sessionStorage
        try {
            const saved = sessionStorage.getItem('scrollPositions');
            if (saved) {
                const positions = JSON.parse(saved);
                const key = window.location.pathname + window.location.search;
                const pos = positions[key];
                if (pos) {
                    window.scrollTo(pos.x, pos.y);
                }
            }
        } catch (e) {
            // Ignore parse errors
        }
    });

    // Save scroll state in history
    let scrollTimeout;
    window.addEventListener('scroll', () => {
        clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(() => {
            const state = window.history.state || {};
            state.scrollX = window.scrollX;
            state.scrollY = window.scrollY;
            window.history.replaceState(state, '');
        }, 100);
    });
})();

Focus Management for Accessibility

// Manage focus for keyboard and screen reader users
(function() {
    function managePageFocus() {
        // Find the main heading or first landmark
        const heading = document.querySelector('h1') ||
                       document.querySelector('h2') ||
                       document.querySelector('[role="main"]') ||
                       document.querySelector('main');

        if (heading) {
            // Make focusable if not already
            if (!heading.hasAttribute('tabindex')) {
                heading.setAttribute('tabindex', '-1');
            }

            heading.focus({ preventScroll: true });

            // Announce page change to screen readers
            const announcer = document.getElementById('page-announcer') ||
                            createAnnouncer();
            const pageTitle = document.title || heading.textContent;
            announcer.textContent = `Navigated to ${pageTitle}`;
        }
    }

    function createAnnouncer() {
        const announcer = document.createElement('div');
        announcer.id = 'page-announcer';
        announcer.setAttribute('aria-live', 'polite');
        announcer.setAttribute('aria-atomic', 'true');
        announcer.style.cssText = `
            position: absolute;
            width: 1px; height: 1px;
            overflow: hidden;
            clip: rect(0, 0, 0, 0);
        `;
        document.body.appendChild(announcer);
        return announcer;
    }

    // Run on page load
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', managePageFocus);
    } else {
        managePageFocus();
    }

    // Also run after dynamic content updates
    document.addEventListener('turbo:load', managePageFocus);
    document.addEventListener('htmx:afterSwap', managePageFocus);
})();

Common Mistakes

  1. Not managing focus on navigation. Screen reader users are sent back to the top of the page without knowing navigation occurred. Always move focus to the main heading after page load.
  2. Losing scroll position on back navigation. Users expect to return to their previous scroll position when pressing the back button. Use History API or sessionStorage to preserve scroll positions.
  3. No loading indication. Full-page reloads take time. Users need visual feedback that their click registered. Add a loading bar or spinner for navigation.
  4. Blocking navigation with JavaScript. Do not prevent default link behavior or add click handlers that delay navigation. Navigation should start immediately.
  5. Ignoring bfcache (Back-Forward Cache). The browser can cache pages for instant back/forward navigation. Set correct Cache-Control headers and avoid beforeunload handlers that disable bfcache.

Practice Questions

  1. What is the sequence of events during an MPA page transition?
  2. How do you preserve scroll position when the user navigates back?
  3. Why is focus management important for accessibility during page transitions?
  4. How does the bfcache improve navigation performance?
  5. What are some ways to provide loading feedback during page transitions?

Challenge: Add progressive enhancement to an MPA: implement a loading bar that appears on link clicks, preserve scroll position across navigations using sessionStorage, manage focus for screen readers by moving focus to the h1 on page load, and verify that the bfcache works correctly for back/forward navigation.

FAQ

What is bfcache and how does it affect page transitions?

bfcache (Back-Forward Cache) caches the complete page state in memory. When the user navigates back, the page restores instantly without a server request. Avoid beforeunload handlers to enable bfcache.

Can I animate MPA page transitions?

Yes, but animations are limited because the browser clears the page before showing new content. You can use Shared Element Transitions API (cross-document) or view transitions for smooth animations.

How do I handle form submissions during navigation?

Use the Post-Redirect-Get pattern. The form posts to the server, the server processes it, and redirects to a success page. This prevents duplicate submissions on page refresh.

Does MPA navigation feel slower than SPA?

Perceptually yes, because the page flashes white. But the actual time to show meaningful content can be faster in MPAs since the server sends complete HTML immediately.

How do I prevent the white flash during navigation?

Use an in-memory page cache, show a loading screen immediately on click, or use Turbo Drive/HTMX for partial page replacements that avoid full reloads.

Mini Project

Enhance an MPA with smooth navigation features: a CSS loading bar that activates on link clicks and completes on page load, scroll position preservation using sessionStorage for the last 10 pages, focus management that moves keyboard focus to the page heading on load, and a bfcache test to confirm pages restore instantly on back navigation.

What's Next

You understand page transitions. Now learn about Form Submissions in MPAs with validation, error handling, and the PRG pattern.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro