Preserve navigation scroll position/toggle state on page change #971
Replies: 17 comments 3 replies
-
Thanks for the issue. Would be nice to improve this indeed. For a simple solution in your own project, you can add scripts using the head: [{ tag: 'script', attrs: { src: '/my-script.js' } }], |
Beta Was this translation helpful? Give feedback.
-
Sorry, I missed that. I thought I had scoured for places to insert stuff. I would still prefer a way to inline it (I know I can inline it in that (() => {
const key = 'toc-position';
let timeout = 0;
let position = { scrollTop: 0 };
const sidebar = document.querySelector('#starlight__sidebar > .sidebar-content');
if (sidebar) {
const data = sessionStorage.getItem(key);
if (data) {
try {
const obj = JSON.parse(data);
position = {
...position, ...obj,
};
sidebar.scrollTop = position.scrollTop || 0;
}
catch (err) {
// ...
}
}
sidebar.addEventListener('scroll', () => {
position.scrollTop = sidebar.scrollTop;
if (!timeout) {
timeout = window.setTimeout(() => {
sessionStorage.setItem(key, JSON.stringify(position));
timeout = 0;
}, 100);
}
});
}
})(); |
Beta Was this translation helpful? Give feedback.
-
I just tried this and it works great. Thank you @duncanwerner for sharing. This means that the scoll position is not correct in this case. Am I missing something or is it not possible to save this state at the moment? |
Beta Was this translation helpful? Give feedback.
-
Here's our current version, which saves the detail state: https://gist.github.com/duncanwerner/d49fd9574ecd7a26d1febd0077dd012b Example: You are welcome to use it, just remember that it is fragile to future changes in Starlight. |
Beta Was this translation helpful? Give feedback.
-
Wondering, any progress on this? |
Beta Was this translation helpful? Give feedback.
-
Yes, I wish scroll state saving was built into starlight |
Beta Was this translation helpful? Give feedback.
-
I had a similar issue, and also worse, with websites having a high pages count, the menu building on each page slowed down my build time considerably. So I designed this client side shamefully injected menu with vanilla js, the advantage is the open closed state storage and the menu data are fetched only once https://github.com/MicroWebStacks/astro-big-doc/blob/main/src/layout/client_menu.js |
Beta Was this translation helpful? Give feedback.
-
Any reasons why the solution of @duncanwerner couldn't be merged into the project? Looks like a solid implementation to me. |
Beta Was this translation helpful? Give feedback.
-
Glad to see this being discussed, it would be great to get a fix of some sort merged in! I tried out some modifications building on @duncanwerner's work because I ran into issues with "flash of un-scrolled content" (FOUSC?) on Safari. https://gist.github.com/kitschpatrol/f40f3382d9b96ab23d826453aa3af1ca (see update in next comment) Basic objectives:
Changes:
To include the script in the page, I add the I also messed with creating a web component to see if I could get better control over the lifecycle to avoid FLOUSC, but didn't have any luck. Open questions for this approach:
Improvements needed:
One other thing I noticed is that by default, the div#starlight__sidebar {
overflow: hidden;
}
div#starlight__sidebar > div.sidebar-content {
overflow-y: auto;
} Anyway open to and feedback on this approach, or other proposals that would get us closer to a PR. 👍 |
Beta Was this translation helpful? Give feedback.
-
Take two, simpler to inline the script right after the sidebar is added vs. keeping the script in the head and running the mutation observer. Example of change to https://gist.github.com/kitschpatrol/a517d90386c0ebc13b465c6a7ef662fa |
Beta Was this translation helpful? Give feedback.
-
I made a few more tweaks (got rid of my decadent |
Beta Was this translation helpful? Give feedback.
-
Here's my implementation if it can help anyone. I have a menu that maps a blog collection into links and when the user clicks on it, they are directed to the blog page with the menu going to the side. I wanted to preserve the scroll in the menu when the user clicks on another post. Seems to work fine with view transitions. I put this code in my root layout. <script>
function preserveScriptbookMenuScroll () {
const sidebar = document.getElementById("scriptbook");
if (!sidebar) return;
// Get previous scroll position (set to "0" if not found)
let scrollPositionString = sessionStorage.getItem("scrollPositionScriptbookMenu")
if (!scrollPositionString) {
scrollPositionString = "0";
}
let scrollPosition = parseInt(scrollPositionString);
// Set scroll position
sidebar.scrollTop = scrollPosition;
// Listen to changes in the scroll position and save it to session storage
sidebar.addEventListener("scroll", () => {
scrollPosition = sidebar.scrollTop;
sessionStorage.setItem("scrollPositionScriptbookMenu", scrollPosition.toString());
});
}
const recurringScripts = () => {
preserveScriptbookMenuScroll();
}
document.addEventListener('astro:page-load', () => {
recurringScripts();
});
</script> |
Beta Was this translation helpful? Give feedback.
-
Just in case anyone is still looking for a solution here that works with view transitions, I've written something that seems to work well. The following code doesn't require storing the scroll position in session storage and also scrolls to the correct menu item when using the global search (as this selects a menu item without the user actually scrolling to it). I've created this custom override component for the sidebar, this just uses the default component but adds a script which checks if the menu item is visible and scrolls to it if not.
---
import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/Sidebar.astro";
---
<Default {...Astro.props}><slot /></Default>
<script>
function isElementVisibleInContainer(element: Element, container: Element) {
const elementRect = element.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
return (
elementRect.top >= containerRect.top &&
elementRect.left >= containerRect.left &&
elementRect.bottom <= containerRect.bottom &&
elementRect.right <= containerRect.right
);
}
// Function to persist scroll position on page change
function setScrollPosition() {
const sidebar = document.querySelector("#starlight__sidebar");
const selectedMenuItem = document.querySelector(
"#starlight__sidebar a[aria-current='page']",
);
if (
sidebar &&
selectedMenuItem &&
!isElementVisibleInContainer(selectedMenuItem, sidebar)
) {
selectedMenuItem.scrollIntoView();
}
}
document.addEventListener("astro:page-load", () => {
setScrollPosition();
});
</script> |
Beta Was this translation helpful? Give feedback.
-
Astro docs https://docs.astro.build/en/getting-started/ has the same problem for now, UX is not good enough. |
Beta Was this translation helpful? Give feedback.
-
If you're using Astro v4 and View Transitions, here's a potential solution that leverages the lifecycle events of <script>
document.addEventListener("astro:before-swap", () => {
sessionStorage.setItem("scrollPosition", window.scrollY.toString());
});
document.addEventListener("astro:after-swap", () => {
const scrollPosition = sessionStorage.getItem("scrollPosition");
if (scrollPosition) {
window.scrollTo({
top: parseInt(scrollPosition, 10),
behavior: "instant",
});
sessionStorage.removeItem("scrollPosition");
}
});
</script> We're saving the current scroll position in session storage in the |
Beta Was this translation helpful? Give feedback.
-
If you read down here you are probably still interested in further solution alternatives.
have look at the Starlight support of the Bag of Tricks: https://events-3bg.pages.dev/jotter/starlight/guide/ |
Beta Was this translation helpful? Give feedback.
-
Starting with Starlight |
Beta Was this translation helpful? Give feedback.
-
What version of
starlight
are you using?0.4.1
What version of
astro
are you using?2.7.4
What package manager are you using?
npm
What operating system are you using?
Linux (WSL)
What browser are you using?
Chrome/Edge/Firefox
Describe the Bug
If you have a very long set of navigation links, the scroll position resets on page transitions. The same is true for toggle state, which makes the page look jumpy on transitions even without a lot of pages. It's not the end of the world but it is annoying. It would be preferable to maintain these across page changes.
This can be done using some script and it's not too heavy weight. It would be nice if this were included. Alternatively if there was a way to add custom scripts to the default layout -- something like
customCss
that would let me insert an Astro component or even just some TS -- I could do it myself without forking starlight.Link to Minimal Reproducible Example
No response
Participation
Beta Was this translation helpful? Give feedback.
All reactions