From 82abc95d364e33062b8775054f041330fad5b09a Mon Sep 17 00:00:00 2001 From: Jairus Joer Date: Fri, 24 Oct 2025 20:49:01 +0200 Subject: [PATCH] feat: implement view transition functionality --- quartz/components/scripts/spa.inline.ts | 45 +++++++++++++------------ quartz/components/scripts/util.ts | 9 +++++ quartz/styles/base.scss | 4 +++ 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts index 22fcd72b4..d827394d0 100644 --- a/quartz/components/scripts/spa.inline.ts +++ b/quartz/components/scripts/spa.inline.ts @@ -1,6 +1,6 @@ import micromorph from "micromorph" import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path" -import { fetchCanonical } from "./util" +import { fetchCanonical, startViewTransition } from "./util" // adapted from `micromorph` // https://github.com/natemoo-re/micromorph @@ -102,31 +102,34 @@ async function _navigate(url: URL, isBack: boolean = false) { html.body.appendChild(announcer) // morph body - micromorph(document.body, html.body) + startViewTransition(() => { + micromorph(document.body, html.body) - // scroll into place and add history - if (!isBack) { - if (url.hash) { - const el = document.getElementById(decodeURIComponent(url.hash.substring(1))) - el?.scrollIntoView() - } else { - window.scrollTo({ top: 0 }) + // scroll into place and add history + if (!isBack) { + if (url.hash) { + const el = document.getElementById(decodeURIComponent(url.hash.substring(1))) + el?.scrollIntoView() + } else { + window.scrollTo({ top: 0 }) + } } - } - // now, patch head, re-executing scripts - const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])") - elementsToRemove.forEach((el) => el.remove()) - const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])") - elementsToAdd.forEach((el) => document.head.appendChild(el)) + // now, patch head, re-executing scripts + const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])") + elementsToRemove.forEach((el) => el.remove()) + const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])") + elementsToAdd.forEach((el) => document.head.appendChild(el)) - // delay setting the url until now - // at this point everything is loaded so changing the url should resolve to the correct addresses - if (!isBack) { - history.pushState({}, "", url) - } + // delay setting the url until now + // at this point everything is loaded so changing the url should resolve to the correct addresses + if (!isBack) { + history.pushState({}, "", url) + } + + notifyNav(getFullSlug(window)) + }) - notifyNav(getFullSlug(window)) delete announcer.dataset.persist } diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index f71790104..a0bb5da86 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -44,3 +44,12 @@ export async function fetchCanonical(url: URL): Promise { const [_, redirect] = text.match(canonicalRegex) ?? [] return redirect ? fetch(`${new URL(redirect, url)}`) : res } + +/** + * Wraps a DOM update in a View Transition if supported by the browser. + * Falls back to immediate execution if the API is unavailable. + * @param callback - The function containing DOM updates to animate + */ +export function startViewTransition(callback: () => void): void { + document.startViewTransition?.(() => callback()) ?? callback() +} diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 391139cdd..968e5a809 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -4,6 +4,10 @@ @use "./syntax.scss"; @use "./callouts.scss"; +::view-transition { + pointer-events: none; +} + html { scroll-behavior: smooth; text-size-adjust: none;