kit: Page scroll position not reset to top on navigation (regression)

Describe the bug

When navigating to a new page, if you are already scrolled partway down the page, the next page load will keep the current scroll position and not scroll back to the top.

I believe this broke with next-193. When I install next-192, this does not happen – navigating to a new page always scrolls to the top, just like regular browser navigation.

Reproduction

Init the SvelteKit demo project. Add the following paragraph in about.svelte after the last paragraph.

<p>Go <a href="/">home</a></p>

Make the browser window small enough so that the page scrolls and scroll to the bottom of the page. Click the Home link. The Home page loads, but does not scroll to the top (i.e. the nav bar is not visible).

scroll-bug.

Tested in Chrome and Firefox on Windows 10.

Logs

No response

System Info

System:
    OS: Linux 4.19 Ubuntu 20.04.1 LTS (Focal Fossa)
    CPU: (4) x64 Intel(R) Core(TM) i7-6600U CPU @ 2.60GHz
    Memory: 7.79 GB / 12.40 GB
    Container: Yes
    Shell: 5.0.17 - /bin/bash
  Binaries:
    Node: 14.17.0 - ~/.nvm/versions/node/v14.17.0/bin/node
    Yarn: 1.22.5 - /usr/bin/yarn
    npm: 8.1.1 - ~/.nvm/versions/node/v14.17.0/bin/npm
  Browsers:
    Chrome: 89.0.4389.90
  npmPackages:
    @sveltejs/kit: ^1.0.0-next.193 => 1.0.0-next.193
    svelte: ^3.34.0 => 3.44.1

Severity

serious, but I can work around it

Additional Information

No response

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 39
  • Comments: 82 (25 by maintainers)

Commits related to this issue

Most upvoted comments

These are great comments! This was my situation and what worked for me:

I have two independently scrolling panels in the __layout.svelte file:

<div id="layout">
  <div id="sidebar">
    <Sidebar />
  </div>
  <main id="main">
    <slot />
  </main>
</div>

<style>  
  #layout {
    height: 100vh;
    display: flex;

    & #sidebar {
      width: 20%;
      overflow-y: auto;
    }

    & #main {
      flex: 1;
      padding: 0 20px 100px 20px;
      overflow-y: auto;
    }
  }
</style>

The sidebar element contains the navigation and the <main> element contains my content. (The content in the <main> element is the part whose scroll position needs to be reset to the top when a user navigates to a new page.) The issue for me was when I would scroll down one page and then click on another page in the navigation, then the scroll position of the content in the <main> element would remain scrolled down the page. Borrowing some of the ideas posted above, I was able to fix it by adding the afterNavigation hook in my __layout.svelte file, like this:

<script>
  import { afterNavigate } from "$app/navigation";
  import Sidebar from "./_sidebar.svelte";

  afterNavigate(() => {
    document.getElementById("main").scrollTop = 0;
  });
</script>

Since the scroll position of the content in the <main> element is what needed to be reset, I referenced the <main> element inside the afterNavigation hook and reset its scroll position.

I hope that helps someone else.

This fixed it for me:

import { afterNavigate, beforeNavigate } from '$app/navigation';

beforeNavigate(() => {
    document.documentElement.style.scrollBehavior = 'auto';
});
afterNavigate(() => {
    document.documentElement.style.scrollBehavior = '';
});

I created a reproduction repo that you can access here.

I uploaded a demo in Cloudflare Pages. Here is a demo functioning correctly. When you scroll to the bottom of the page and click on the link, you arrive at the top of the new page.

If I add the following css, the bug appears:

html {
	height: 100%;
	overflow: hidden;
}

body {
	height: 100%;
	overflow: auto;
}

You can see a demo of the bug here. Now when we scroll to the bottom of the page, when we click the link we no longer arrive at the top of the new page, but stay somewhere in the middle.

It’s a bit hacky, but I got around it by adding the following to my root +layout.svelte file:

let scrollHistory = []

beforeNavigate((navigation) => {
         // I have everything wrapped in a `main` tag so that's the part I need to scroll
	const main = document.querySelector('main');

	scrollHistory.push({
		to: navigation.to,
		from: navigation.from,
		scrollY: main.scrollTop
	});
});

afterNavigate((navigation) => {
	const main = document.querySelector('main');
	const routeHistory = scrollHistory.find((history) => {
		return history.from.url.pathname === navigation.to.url.pathname;
	});

        // I only want to revert the scroll when going back in the history
	if (routeHistory && navigation.type == 'popstate') {
		main.scrollTo(0, routeHistory.scrollY);
		// I reset the scrollHistory here so that the array doesn't store lots of unneeded values, but could also just add and remove the necessary ones if the page structure is more complex
		scrollHistory = [];
	} else {
	        // if it's a page that isn't in the scrollHistory, simply scroll to the top of the page
		main.scrollTo(0, 0);
	}
});

Was wondering for a moment if it’s feasible to check if html has overflow: hidden, and if so, traverse the dom a few elements (bf search depth 3), but that’s a) not very performant b) can result in the wrong scrollbar getting scrolled. Imagine a navbar to the left at the same DOM depth than the main content. It gets found first and is scrolled when it should be the main content that’s scrolled - that’s worse than not scrolling at all.

I have a PR in the works that tries to address this problem. It might not be the right solution but I would appreciate any feedback on it.

The solution involves checking if html and body are scrollable, and provides an alternative HTML attribute such as data-sveltekit-main-scrollable so the developer can easily tell SvelteKit where the main scrolling area.

I bumped into this issue on my personal repo, using sveltekit 1.5.0 and svelte 3.54.0

here’s a minimal reproducible sample crowdozer/svelte-kit-scroll-test happens on brave and chrome, haven’t tried others

the gist is this inner “page” div never scrolls on navigation

<div class="flex h-full flex-col overflow-auto">
	<header>
		<!-- header content -->
	</header>
	<div id="page" class="flex grow flex-col">
		<div class="grow">
			<!-- main page content here -->
		</div>
		<footer>
			<!-- footer content -->
		</footer>
	</div>
</div>

my workaround has been to put this in the root layout, now it works fine

import { afterNavigate } from '$app/navigation';

afterNavigate(() => {
	document.getElementById('page')?.scrollTo(0, 0);
});

Yup, this is a known issue. Sveltekit doesn’t know which area to scroll if it’s not the window. https://github.com/sveltejs/kit/pull/8723 attempts to resolve this by adding an attribute you can use to specify the main scrolling area, but it might not be the most clean solution.

I can reproduce this issue with the given repository. The native scrollTo function that is used to scroll to the top does not work if the rootElement in the __layout.svelte has overflow: auto; and max-height: 100vh;. In the repository @akkie provided it is caused by the ‘Drawer’ component. So something like this:

<div style="max-height: 100vh; overflow-y: auto;">
	<main>
		<slot />
	</main>
	<Footer />
</div>

in the _layout.svelte prevents the native scrollTo function to work properly (https://stackoverflow.com/questions/1174863/javascript-scrollto-method-does-nothing)

However I’m not quite sure if SvelteKit can do anything about that as this issue is caused by CSS inside the SvelteKit App.

I bumped into this issue on my personal repo, using sveltekit 1.5.0 and svelte 3.54.0

here’s a minimal reproducible sample https://github.com/crowdozer/svelte-kit-scroll-test happens on brave and chrome, haven’t tried others

the gist is this inner “page” div never scrolls on navigation

<div class="flex h-full flex-col overflow-auto">
	<header>
		<!-- header content -->
	</header>
	<div id="page" class="flex grow flex-col">
		<div class="grow">
			<!-- main page content here -->
		</div>
		<footer>
			<!-- footer content -->
		</footer>
	</div>
</div>

my workaround has been to put this in the root layout, now it works fine

import { afterNavigate } from '$app/navigation';

afterNavigate(() => {
	document.getElementById('page')?.scrollTo(0, 0);
});

Adding a timeout to the window scrollTo worked for us:

  onMount(async () => {
    setTimeout(() => {
      window.scrollTo(0, 0);
    }, 0);

@raymclee for now, downgrade to 1.0.0-next.192. It’s only 1 version back so you shouldn’t be missing much (changelog)

npm i @sveltejs/kit@1.0.0-next.192

Still broken on Firefox. Seems to work on Chrome.

Thanks @s3812497. I’d consider that a workaround, not a bug fix. But appreciate you pointing to that code.

I’m here to report that I’m seeing this issue on the sveltekit sverdle demo (as well as my site) when I use scroll-behavior: smooth on firefox version 112.0.2 (64-bit) when I use scroll-behavior: smooth on the html.

experiencing this while using height:100% on both html and body. can’t really remove those because those are already fixed for 100vh in mobile brower issue.

We noticed we had this issue as well; after checking we saw we were using height:100vh; on a div we replaced it for min-height:100vh; and this stopped happening

I’m still facing this issue in the Svelte 3.48.0 on chrome and firefox. Until the fix comes out I’m using the onMount function with window.scrollTo(x-coord, y-coord).

Looks to me like setting scroll-behavior: smooth breaks scrolling back up on page switch in Firefox, too, as reported by another @AaronNBrock.

Here’s my dirty fix in +layout.svelte:

<script>
  // Fix SvelteKit scrolling issue
  import { browser } from "$app/environment";

  beforeNavigate(async (nav) => {
    if (!browser) return;
    document.getElementsByTagName("html")[0].classList.add("pageSwitch");
  })
  afterNavigate(async (nav) => {
    if (!browser) return;
    await tick();
    document.getElementsByTagName("html")[0].classList.remove("pageSwitch");
  });
</script>

<style>
  :global(html.pageSwitch) {
    scroll-behavior: auto;
  }
</style>

In my case, scroll behavior is only broken when navigating from root to another page, and only on the initial load. Refreshing the page will then scroll to the top. I’m running SvelteKit 2.3.2 and Svelte 4.2.8.

I tried many of the suggested fixes in this thread, but the only one that worked for me was using the following, or minor variants (e.g. binding to an element and scrollIntoView(), not checking nav type, etc.). The timeout is necessary, and setting the scroll behavior breaks it again.

afterNavigate((nav) => {
  if (nav.type === 'link') {
    setTimeout(() => {
      window.scrollTo(0,0);
    })
  }
})

However, that fix can result in a content flash that’s a bit annoying on some loads due to the nature of setTimeout(). Inspired by that fix, the following also works for me and doesn’t result in any content flashing.

afterNavigate(async (nav) => {
  if (nav.type === 'link') {
    await tick();
    window.scrollTo(0, 0);
  }}
);

Seems odd, since as far as I can tell, svelteKit natively does something very similar (see here).

I have a similar problem. When a navigate from the root page to a details page, the scroll position is not reset. My suspicion is that this may be caused by nested layouts, but I did not verify this.

I use the following snippet in my +layout.svelte to workaround this bug by forcing a re-render when the URL changes.

{#key $page.url}
<div bind:this={contentContainer} class="flex-1 overflow-auto">
	<slot />
</div>
{/key}

It functions the same way people are describing the iOS issue in that it only appears when I apply the ’scroll-behavior: smooth’. I was able to reproduce it with a fresh install of the sverdle demo by only editing the style.css to add the smooth scrolling.

I can’t seem to reproduce this on the same version of Firefox (macos). https://stackblitz.com/github/s3812497/sveltejs-kit-2733?file=README.md

Can you reproduce it using the repository I’ve shared, or provide your own reproducible repository?

Which issue are you referring to that causes scroll to break?

  1. Setting overflow: hidden on the html element.
  2. iOS webkit scroll-behavior: smooth bug.

If it’s different from both of these, can you provide a reproducible?

It functions the same way people are describing the iOS issue in that it only appears when I apply the ’scroll-behavior: smooth’. I was able to reproduce it with a fresh install of the sverdle demo by only editing the style.css to add the smooth scrolling.

Edit: Here’s the recording:

2023-04-30-12-26-06-firefox_u6CB

Still occurring for us on 1.2.1. (Actually, we were still on ^1.0.11 😬 - now up-to-date at ^1.2.9! )

We have a workaround inspired by https://github.com/sveltejs/kit/issues/2733#issuecomment-1341340679. Add a div on the top of our scrollable element (you could use an existing element if you have one appropriate) and use scrollIntoView:

<div bind:this={top} />

let top: HTMLElement;
afterNavigate((nav) => {
  if (nav.type === "link") {
    top?.scrollIntoView();
  }
});

We’re checking type === "link" because we don’t want to always scroll to top, for example when we’re using goto with noScroll.

For us, the problem seems to happen consistently on mobile (iOS Chrome and Safari, at least) but less-frequently on desktop browsers. (No idea why it is intermittent on desktop.)

Our layout is similar to the one nhunzaker describes above. On desktop browsers, most of the time, the new pages load correctly, at the top.

Here’s a demo of the issue:

https://user-images.githubusercontent.com/101564068/214556754-91d4b602-6531-4c80-9439-6e65c58d69b0.mov

Chiming in…

Seeing scroll-to-top behavior not working in iOS/safari, but fine on desktop/chrome.

For iOS/safari at least, it looks like a webkit regression, basically that scroll-behavior: smooth + window.scrollTo (programmatic scrolling) doesn’t work: https://bugs.webkit.org/show_bug.cgi?id=238497 (Additional discussion: https://developer.apple.com/forums/thread/703294)

Temporary workaround I’ve implemented is a media query to reset scroll-behavior, e.g. @media (max-width: 640px) { html { scroll-behavior: auto; } } which makes goto() calls not dump me halfway down the subsequent page.

I’m running into this but only in Firefox. Removing height 100vh/100% from html and body does not fix it for me.

Same here: https://github.com/babakfp/svelte-movies

"@sveltejs/kit": "1.0.0-next.328",

Video: https://user-images.githubusercontent.com/97315614/168525690-2de734dc-972e-42ec-976c-19b4a42c1855.mp4


Update 1

Removing 100vh from <body> fixed it for me

I removed height: 100% on html and body, and it fixed. Watch this video:

https://user-images.githubusercontent.com/97315614/168527428-06c58837-15c6-4ae8-9f1f-eb0c4ab93576.mp4


Update 2

In update 1 you saw that when I navigate back to the home page, the scroll position starts where I left it (it’s the expected behavior). I added another section of movies and now there is this random behavior that you can see in this video:

https://user-images.githubusercontent.com/97315614/168645174-8eac1e83-7e30-4d4b-bc54-b69197f11c40.mp4

Removing 100vh from <body> fixed it for me

Removing scroll-behavior: smooth; from html was what helped me work around the issue for now using next.260 with Firefox on MacOS Monterey.

@nikosgpet could you maybe provide a minimal repo? I already created a PR and I’d like to check if my changes also fix your usecase.

I think this can be handled by SvelteKit as well. But making something like this configurable would bloat the configuration. In my opinion the page reset should just work without the user having to think about it. However we could improve the page reset logic by checking if there’s a root element in the html body that has a scrollHeight > 0 and if so setting it to 0. I’ll try that solution out and create a PR if it is feasible.

@nhe23 I just upgraded to 1.0.0-next.224 and the scroll position issue seems resolved. I needed to fix that $page.path->$page.url.pathname thing.

However I couldn’t upgrade beyond version 1.0.0-next.224. If I tried 1.0.0-next.225 or higher, I get the same sass error as you. I’m not sure what that is, but I’ll debug that separately.

Thanks for you help!

@johnknoop can you provide code and instructions for reproducing? as stated earlier, it’s hard to debug without the code

Maybe sveltekit as a cool tool could emit a warning if someone uses scrollTo() and has the given styles applied in the root component. 😃

It’s very hard to debug the issue with just links to a website. It would be great to have a minimal repro (either a github repo or stackblitz link) so we can track this down.

Currently, the code to scroll to top is here. The if (this.autoscroll) check is based on whether you’ve called disableScrollHandling().

In my tests of the https://painhas.com website, I can confirm that Kit is calling scrollTo(0, 0), which should scroll to the top. By injecting this code in the console:

const _scroll = window.scrollTo
window.scrollTo = function() {
  console.log('scrollTo', arguments)
  _scroll.apply(window, arguments)
}

I cannot reproduce this issue. Scroll position is reset to the top as it should be for me. Im also using MacOS and the current SvelteKit Version (v1.0.0-next.242) and tested with Chrome and Safari using links to different routes (explicitly not “/”). It would be great if someone who has this issue could provide a repository to reproduce and detailed system information.

Fixed for me in @sveltejs/kit@1.0.0-next.199.

@jgrieger We haven’t changed much in the router since this fix, so I’m guessing the root issue is the same as #2794 (newer). I haven’t found a way around it yet.

I’m having same problem. What is the workaround please?