Skip to content

Scrollspy: Browser Back Button navigates away from page instead of returning to previous position #749

@don-esteban

Description

@don-esteban

Summary

The Scrollspy plugin uses history.replaceState() in its scrollTo() method (source: core.ts line 164). This means clicking TOC links does not create browser history entries. When the user presses the Back button, they expect to return to the previous anchor — instead, the browser navigates away from the page entirely.

Steps to Reproduce

  1. Open https://preline.co/docs/scrollspy.html
  2. Click any anchor link in the right-side (large screen) navigation (e.g. https://preline.co/docs/scrollspy.html#example-with-nested-nav)
  3. Press the browser Back button

Demo Link

https://preline.co/docs/scrollspy.html

Expected Behavior

Expected: Browser scrolls back to https://preline.co/docs/scrollspy.html.

Actual Behavior

Actual: Browser navigates away from the page (back to wherever you came from).

Root cause

In scrollTo(), the plugin calls:

window.history.replaceState(null, null, link.getAttribute('href'));

replaceState updates the URL but does not create a history entry. Changing this to pushState would restore standard anchor navigation behavior. Each clicked anchor gets a history entry, and Back/Forward step through them.

Suggested fix

In src/plugins/scrollspy/core.ts, line 164, change:

window.history.replaceState(null, null, link.getAttribute('href'));

to:

window.history.pushState(null, null, link.getAttribute('href'));

A popstate listener would also be needed to scroll to the target on Back/Forward:

window.addEventListener("popstate", () => {
    const id = location.hash.slice(1);
    const el = id ? document.getElementById(id) : null;
    if (el) el.scrollIntoView({ behavior: "smooth" });
});

Workaround

Add a capture-phase click listener that calls pushState before the Scrollspy handler runs. Scrollspy's subsequent replaceState harmlessly overwrites the same hash:

document.addEventListener("click", (e) => {
    const link = e.target.closest('a[href^="#"]');
    if (link) history.pushState(null, "", link.getAttribute("href"));
}, true);

window.addEventListener("popstate", () => {
    const id = location.hash.slice(1);
    const el = id ? document.getElementById(id) : null;
    if (el) el.scrollIntoView({ behavior: "smooth" });
});

Environment

  • Preline UI v4.1.3
  • Tested in Chrome 134, Safari 18.4, macOS

Screenshots

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions