How to identify forward vs backward navigation with browser history API

I need help figuring out whether a popstate event was triggered by clicking the browser’s back button or forward button. Right now I’m storing timestamps in the history state when pages get pushed, and this works fine most of the time. But there’s a problem when someone navigates back a few pages and then refreshes the current page. The timestamp gets updated on refresh, so when they go forward again, my animation logic gets confused because it thinks they’re going backwards.

var NavigationHandler = function (container) {
    var self = this;
    
    self.push = function (path, content, pageTitle) {
        var timestamp = Date.now();
        
        clearPrevious();
        content.setAttribute('data-timestamp', timestamp);
        container.appendChild(content);
        executePageScripts(content);
        
        if (pageTitle) {
            document.title = pageTitle;
        }
        
        history.pushState({
            html: content.innerHTML,
            timestamp: timestamp,
            title: pageTitle
        }, null, '?page=' + timestamp);
    };
    
    function executePageScripts(element) {
        var scriptTags = element.querySelectorAll('script');
        for (var j = 0; j < scriptTags.length; j++) {
            var func = new Function(scriptTags[j].innerHTML);
            func.call(window);
        }
    }
    
    function setupCurrentPage() {
        var currentTimestamp = Date.now();
        
        if (history.state && history.state.timestamp) {
            currentTimestamp = history.state.timestamp;
        }
        
        var activePage = container.children[container.children.length - 1];
        activePage.setAttribute('data-timestamp', currentTimestamp);
        var pageContent = activePage ? activePage.innerHTML : '';
        
        history.replaceState({
            html: pageContent,
            timestamp: currentTimestamp
        }, null, location.href);
    }
    
    function clearPrevious(isBackward) {
        if (container.children.length < 1) return;
        
        var activePage = container.children[container.children.length - 1];
        if (!activePage) return;
        
        activePage.addEventListener("animationend", function() {
            if (activePage.parentElement) {
                activePage.parentElement.removeChild(activePage);
            }
        });
        
        if (isBackward === true) {
            activePage.classList.add('backward');
        } else {
            activePage.classList.remove('backward');
        }
        activePage.classList.add('exit');
    }
    
    window.addEventListener('popstate', function(event) {
        if (!event.state) return;
        
        var activePage = container.children[container.children.length - 1];
        var activeTimestamp = activePage.attributes['data-timestamp'].value;
        var newPage = document.createElement('div');
        
        newPage.innerHTML = event.state.html;
        newPage.setAttribute('data-timestamp', event.state.timestamp);
        newPage.className = 'history-page';
        
        var isGoingBack = activeTimestamp > event.state.timestamp;
        if (isGoingBack) {
            newPage.classList.add('backward');
        }
        
        if (event.state.title) {
            document.title = event.state.title;
        }
        
        clearPrevious(isGoingBack);
        container.appendChild(newPage);
        executePageScripts(newPage);
    });
    
    setupCurrentPage();
};

function createPage() {
    var pageElement = document.createElement('div');
    var currentTime = new Date().toLocaleTimeString();
    pageElement.innerHTML = '<div><h2>' + currentTime + '</h2></div>';
    navHandler.push('?page=' + Date.now(), pageElement, 'New Page');
}

var navHandler = new NavigationHandler(document.getElementById('content'));

Has anyone found a reliable way to detect navigation direction without running into this refresh issue?

Hit this exact problem building a SPA last month. You’re treating history like a simple stack, but browsers maintain their own internal navigation indices.

Here’s what worked: store a unique navigation ID with each state and keep a separate registry in sessionStorage that tracks the order these IDs were created. When popstate fires, check if the incoming state’s ID comes before or after your current one in the registry.

The trick is ditching timestamps or counters that break on refresh. Create immutable identifiers when you first push states, then use your own ordering system for direction. Handles refreshes well since the navigation ID stays put while your registry rebuilds from existing history states.

Not bulletproof with complex tab stuff, but it’ll fix your refresh issue without breaking animations.

Had this exact problem last year. Here’s what worked for me: track the browser’s history length with your state data. When popstate fires, compare the current history.length to what you stored before. Length increased? Probably forward. Same or decreased? Backward.

Edge cases are annoying - new tabs, keyboard shortcuts, etc. But for basic back/forward detection, this beats timestamps every time. Also consider storing a navigation index in sessionStorage as backup. It survives refreshes but clears when you close the tab.

timestamps are pretty unreliable for this. i switched to a simple counter - just increment it on each push, then compare the state counter with your current position. way more reliable than timestamps, especially when page refreshes mess everything up.