sveltekit-hash-repro Svelte Themes

Sveltekit Hash Repro

Reproduction of a SvelteKit navigation issue with hashed links and <base> elements

This is a reproduction repo to illustrate a navigation issue in SvelteKit >=2.21.3. It is still present in 2.50.1.

The Problem

If your app's HTML[^1], specifies a HTML <base href="/"> element, then SvelteKit-managed navigations to paths with a hash in them will behave unexpectedly on non-root (/) routes.

This issue exists since this PR https://github.com/sveltejs/kit/pull/10856 was merged, which released as part of SvelteKit 2.21.3.

Expected behavior

If I have <a href="/other-page#myhash">My link</a> on the / root page, then I expect that link to navigate to the /other-page#myhash route, since this is normal browser behavior.

Observed behavior

Given the above setup, clicking this link will:

  1. "Soft-navigate" to /other-page#myhash using client-side routing
  2. ... but then immediately do a "hard" browser navigation to /#myhash

We end up on /#myhash and not on /other-page#myhash, and a navigation "flash" is seen due to the hard navigation after the client-side navigation.

(tested on the latest Firefox & Helium (Chromium-based) on macOS)

Why this happens, and why it's a bug (the problem)

In SvelteKit, if you navigate to another SvelteKit page using a path like /other-page#myhash in a <a href="/other-page#myhash"> or in a goto('/other-page#myhash') call, then SvelteKit by default internally calls window.location.replace('#myhash') in the reset_focus() function, see this line in client/client.js.

Here, it specifies only the hash extracted from the path as a relative URL. This implicitly assumes that no <base href="..."> element is set, because otherwise, behavior will not be aligned with normal browser behavior, since the originally intended path is ignored by the .replace() call.

Why it works without a <base> element? If no <base href="..." /> element is set, then the relative #myhash will be relative to the current path. Since the client-side history-API-based routing already occurred, we will already be on the target page, and this will just add or replace the hash in the URL without navigating, which is expected.

If a <base href="/${MY_BASE}/"> is set (like in this reproduction), then this take will you to /${MY_BASE}/#myhash, which is unexpected (see below).

Why is this a bug?

In almost all cases where a <base> element is set, the SvelteKit-managed navigation behavior does not align with normal browser behavior expected from anchor links.

Consider a HTML page with <base href="/subfolder/" />.

Two examples:

  1. Clicking a natively handled <a href="/other-page#myhash"> here will still take you to /other-page#myhash, despite the base element. SvelteKit would take you to /subfolder/#myhash.
  2. Clicking a natively handled <a href="other-page#myhash"> would take you to /subfolder/other-page#myhash, while SvelteKit would take you to /subfolder/#myhash.

The conclusion is: window.location.replace('#myhash') ignores all paths, absolute or relative, specified in the link's href, and this should probably not be the case.

Workarounds

  1. Don't use a <base> element. It was in my project for 3 years, before I even worked on it, and I don't know at the moment why it's there.

    However, there might be a legitimate use cases for <base> elements, and the above I think shows that they can't be safely used in SvelteKit at the moment.

  2. Navigating to a link with the keepFocus setting set to true bypasses the reset_focus() function, which in turn bypasses this issue. Alternatively, adding the data-sveltekit-keepfocus attribute to an element on the page with the link achieves the same effect.

Solution?

Would there be any downside to resolving the URL path, including hash, as it was specified, in the location.replace call, instead of just using the hash?

I didn't fully try to understand SvelteKit's navigation logic yet, nor the reasoning behind the PR that introduced this issue. I could definitely be missing something with this suggestion.


[^1]: This reproduction puts a <base> element in app.html, but I assume that if it appears through statically-rendered HTML, or even through dynamically-rendered HTML in the SvelteKit component tree, it will still be a problem.

Top categories

Loading Svelte Themes