I want to support opening an offcanvas like this <a href="#cart">Cart</a>
from any page. When the offcanvas is open, I want to be able to use the Back button to close it and be at the same scroll position where I was before opening it.
This is my base Offcanvas.svelte that wraps the cart content in my /src/routes/+layout.svelte (with an id of cart
)...
<script lang="ts"> import { onMount } from 'svelte' import type { Snippet } from 'svelte' import { fade, fly } from 'svelte/transition' interface Props { id: string title: string onshown?: () => void children?: Snippet } let { id, title, onshown, children }: Props = $props() const titleId = `${id}Title` let isVisible = $state(false) onMount(() => { if (window.location.hash === `#${id}`) { open() } }) export const open = () => { isVisible = true document.body.style.overflow = 'hidden' // Prevent scrolling if (onshown) onshown() } export const close = () => { isVisible = false document.body.style.overflow = '' // Restore scrolling history.back() // clears hash invoked to open modal preserving scroll position } // Handle escape key to close the offcanvas const onkeydown = (event: KeyboardEvent) => { if (isVisible && window.location.hash === `#${id}` && event.key === 'Escape') { close() } } // Handle hash change to open/close the offcanvas const onhashchange = () => { // If hash changes to #id, open (e.g., user navigates forward or direct link) if (!isVisible && window.location.hash === `#${id}`) { open() } // If it's visible and the hash changes away from #id, close (e.g., back button) else if (isVisible && window.location.hash !== `#${id}`) { isVisible = false document.body.style.overflow = '' } } const onbackdropclick = (event: MouseEvent) => { event.stopPropagation() close() }</script><svelte:window {onhashchange} {onkeydown} />{#if isVisible}<div onclick={onbackdropclick} transition:fade={{ duration: 150 }} class="tw:fixed tw:inset-0 tw:bg-black/50 tw:z-1040" role="presentation"></div><div {id} class="tw:translate-x-0 tw:z-1045 tw:fixed tw:top-0 tw:bottom-0 tw:right-0 tw:w-100 tw:max-w-full tw:bg-white tw:border-l tw:border-gray-200 tw:shadow-lg" transition:fly={{ x: 400, duration: 300 }} tabindex="-1" aria-labelledby={titleId} aria-hidden={!isVisible} aria-modal={isVisible} role="dialog"><div class="tw:flex tw:items-center tw:justify-between tw:p-4"><h2 id={titleId} class="tw:!mb-0 tw:!text-xl tw:font-semibold">{title}</h2><button onclick={close} type="button" class="tw:p-0 tw:border-0 tw:!mb-1 tw:!bg-transparent tw:text-plum tw:hover:text-amber-500" aria-label="Close"><svg height="24" width="24"><title>Close offcanvas</title><use href="/images/icons/icons.svg#x" /></svg></button></div><div class="tw:p-4"> {@render children?.()}</div></div>{/if}
It works but stepping back, I'm wondering if this is fundamentally the right approach or whether there's a way to "register" hash routes like #cart, #contact, #newsletter, etc. (without all the folders since these load on top of existing pages).