From 82abc95d364e33062b8775054f041330fad5b09a Mon Sep 17 00:00:00 2001 From: Jairus Joer Date: Fri, 24 Oct 2025 20:49:01 +0200 Subject: [PATCH 1/5] 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; From de33ab6dda501c288d453b59827f26e9ae36671d Mon Sep 17 00:00:00 2001 From: Jairus Joer Date: Sat, 25 Oct 2025 15:42:09 +0200 Subject: [PATCH 2/5] feat: add configuration option for enabling browser-native view transitions --- quartz.config.ts | 1 + quartz/cfg.ts | 6 ++++++ quartz/components/renderPage.tsx | 2 +- quartz/components/scripts/spa.inline.ts | 3 +-- quartz/components/scripts/util.ts | 13 +++++++++---- quartz/components/styles/backlinks.scss | 1 + quartz/components/styles/comments.scss | 3 +++ quartz/components/styles/darkmode.scss | 1 + quartz/components/styles/explorer.scss | 3 ++- quartz/components/styles/footer.scss | 1 + quartz/components/styles/graph.scss | 2 ++ quartz/components/styles/readermode.scss | 1 + quartz/components/styles/recentNotes.scss | 2 ++ quartz/components/styles/toc.scss | 2 ++ 14 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 quartz/components/styles/comments.scss diff --git a/quartz.config.ts b/quartz.config.ts index b3db3d60d..70bb89a2e 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -11,6 +11,7 @@ const config: QuartzConfig = { pageTitle: "Quartz 4", pageTitleSuffix: "", enableSPA: true, + enableViewTransitions: true, enablePopovers: true, analytics: { provider: "plausible", diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 57dff5c75..7dc6e015d 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -56,6 +56,12 @@ export interface GlobalConfiguration { pageTitleSuffix?: string /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ enableSPA: boolean + /** + * `enableSPA: true` required; Whether to enable browser-native view transitions for single-page-app style rendering. + * + * Customizing your animations: https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#customizing_your_animations. + */ + enableViewTransitions: boolean /** Whether to display Wikipedia-style popovers when hovering over links */ enablePopovers: boolean /** Analytics mode */ diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index 3ebfe4879..a71cc92e3 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -235,7 +235,7 @@ export function renderPage( const doc = ( - +
{LeftComponent} diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts index d827394d0..225648d49 100644 --- a/quartz/components/scripts/spa.inline.ts +++ b/quartz/components/scripts/spa.inline.ts @@ -128,9 +128,8 @@ async function _navigate(url: URL, isBack: boolean = false) { } notifyNav(getFullSlug(window)) + delete announcer.dataset.persist }) - - delete announcer.dataset.persist } async function navigate(url: URL, isBack: boolean = false) { diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index a0bb5da86..c3ab774c3 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -46,10 +46,15 @@ export async function fetchCanonical(url: URL): Promise { } /** - * 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 + * Wraps a DOM update in a View Transition if supported by the browser and enabled in config. + * Falls back to immediate execution if the API is unavailable or disabled. */ export function startViewTransition(callback: () => void): void { - document.startViewTransition?.(() => callback()) ?? callback() + const enableViewTransitions = document.body.dataset.viewTransitions === "true" + + if (enableViewTransitions && document.startViewTransition) { + document.startViewTransition(() => callback()) + } else { + callback() + } } diff --git a/quartz/components/styles/backlinks.scss b/quartz/components/styles/backlinks.scss index 478e118d2..b3e094a7e 100644 --- a/quartz/components/styles/backlinks.scss +++ b/quartz/components/styles/backlinks.scss @@ -2,6 +2,7 @@ .backlinks { flex-direction: column; + view-transition-name: backlinks; & > h3 { font-size: 1rem; diff --git a/quartz/components/styles/comments.scss b/quartz/components/styles/comments.scss new file mode 100644 index 000000000..285cbadf2 --- /dev/null +++ b/quartz/components/styles/comments.scss @@ -0,0 +1,3 @@ +.giscus { + view-transition-name: giscus; +} diff --git a/quartz/components/styles/darkmode.scss b/quartz/components/styles/darkmode.scss index b328743d4..6c5980fff 100644 --- a/quartz/components/styles/darkmode.scss +++ b/quartz/components/styles/darkmode.scss @@ -9,6 +9,7 @@ margin: 0; text-align: inherit; flex-shrink: 0; + view-transition-name: darkmode; & svg { position: absolute; diff --git a/quartz/components/styles/explorer.scss b/quartz/components/styles/explorer.scss index 8d9410044..84a859670 100644 --- a/quartz/components/styles/explorer.scss +++ b/quartz/components/styles/explorer.scss @@ -30,9 +30,10 @@ display: flex; flex-direction: column; overflow-y: hidden; - min-height: 1.2rem; flex: 0 1 auto; + view-transition-name: explorer; + &.collapsed { flex: 0 1 1.2rem; & .fold { diff --git a/quartz/components/styles/footer.scss b/quartz/components/styles/footer.scss index 9c8dbf8c1..cf696f4bb 100644 --- a/quartz/components/styles/footer.scss +++ b/quartz/components/styles/footer.scss @@ -2,6 +2,7 @@ footer { text-align: left; margin-bottom: 4rem; opacity: 0.7; + view-transition-name: footer; & ul { list-style: none; diff --git a/quartz/components/styles/graph.scss b/quartz/components/styles/graph.scss index cb1b7b464..23a4a6f65 100644 --- a/quartz/components/styles/graph.scss +++ b/quartz/components/styles/graph.scss @@ -1,6 +1,8 @@ @use "../../styles/variables.scss" as *; .graph { + view-transition-name: graph; + & > h3 { font-size: 1rem; margin: 0; diff --git a/quartz/components/styles/readermode.scss b/quartz/components/styles/readermode.scss index 79332c3b6..d42eabbb8 100644 --- a/quartz/components/styles/readermode.scss +++ b/quartz/components/styles/readermode.scss @@ -9,6 +9,7 @@ margin: 0; text-align: inherit; flex-shrink: 0; + view-transition-name: readermode; & svg { position: absolute; diff --git a/quartz/components/styles/recentNotes.scss b/quartz/components/styles/recentNotes.scss index 726767198..ee206455c 100644 --- a/quartz/components/styles/recentNotes.scss +++ b/quartz/components/styles/recentNotes.scss @@ -1,4 +1,6 @@ .recent-notes { + view-transition-name: recent-notes; + & > h3 { margin: 0.5rem 0 0 0; font-size: 1rem; diff --git a/quartz/components/styles/toc.scss b/quartz/components/styles/toc.scss index 6a7723bdc..1705c8c28 100644 --- a/quartz/components/styles/toc.scss +++ b/quartz/components/styles/toc.scss @@ -6,6 +6,8 @@ overflow-y: hidden; min-height: 1.4rem; flex: 0 0.5 auto; + view-transition-name: toc; + &:has(button.toc-header.collapsed) { flex: 0 1 1.4rem; } From 0787a46e9b0df31bd70009de14fe784d340bfdfa Mon Sep 17 00:00:00 2001 From: Jairus Joer Date: Sat, 25 Oct 2025 15:46:08 +0200 Subject: [PATCH 3/5] feat: add doucmentation for view transitions --- docs/configuration.md | 1 + docs/features/View Transitions.md | 63 +++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 docs/features/View Transitions.md diff --git a/docs/configuration.md b/docs/configuration.md index c12a8a562..bc0dbc72e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -23,6 +23,7 @@ This part of the configuration concerns anything that can affect the whole site. - `pageTitle`: title of the site. This is also used when generating the [[RSS Feed]] for your site. - `pageTitleSuffix`: a string added to the end of the page title. This only applies to the browser tab title, not the title shown at the top of the page. - `enableSPA`: whether to enable [[SPA Routing]] on your site. +- `enableViewTransitions`: whether to enable browser-native [[View Transitions]] on your site. - `enablePopovers`: whether to enable [[popover previews]] on your site. - `analytics`: what to use for analytics on your site. Values can be - `null`: don't use analytics; diff --git a/docs/features/View Transitions.md b/docs/features/View Transitions.md new file mode 100644 index 000000000..3c47054c4 --- /dev/null +++ b/docs/features/View Transitions.md @@ -0,0 +1,63 @@ +--- +title: "View Transitions" +--- + +View Transitions provide smooth, animated page transitions when navigating between pages in Quartz. This progressively enhances the [[SPA Routing]] experience by utilizing the browser-native [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) where available. + +When navigating between pages, if the View Transitions API is supported and enabled, Quartz will wrap the content update in a view transition. This creates smooth, customizable animations as the page content changes, providing a more polished and app-like experience. + +Under the hood, Quartz uses [[SPA Routing]] to intercept link clicks and perform client-side navigation. The View Transitions feature builds on top of this by wrapping the DOM updates in `document.startViewTransition()`, allowing the browser to automatically animate the changes between the old and new page states. + +> [!info] +> View Transitions require [[SPA Routing]] to be enabled in the [[configuration]]. Browsers that don't support the View Transitions API will fall back to instant page updates without animations. + +## Customization + +Quartz assigns appropriate `view-transition-name` properties to its component, allowing you to create custom animations for different parts of the page. + +### Styling Transitions + +You can customize the behavior of these transitions by adding CSS to your `quartz/styles/custom.scss` file. For example: + +```scss title="quartz/styles/custom.scss" +::view-transition-old(root), +::view-transition-new(root) { + animation-duration: 0.5s; +} +``` + +For more advanced customization options, see the [MDN documentation on customizing view transition animations](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#customizing_your_animations). + +### Programmatic Transitions + +When [[creating components|creating your own components]], you can use the `startViewTransition` utility function to wrap DOM updates in a view transition. This ensures your custom scripts respect your view transition settings. + +```typescript +import { startViewTransition } from "./scripts/util" + +// Wrap your DOM update in a view transition +startViewTransition(() => { + // Your DOM updates here + element.textContent = "New content" +}) +``` + +The `startViewTransition` function will automatically use the View Transitions API if it's supported and enabled, or fall back to immediate execution otherwise. + +## Configuration + +View Transitions require both [[SPA Routing]] and the View Transitions feature to be enabled in the [[configuration]]: + +1. Enable [[SPA Routing]]: set the `enableSPA` field to `true` +2. Enable View Transitions: set the `enableViewTransitions` field to `true` + +```typescript title="quartz.config.ts" +const config: QuartzConfig = { + configuration: { + // ... + enableSPA: true, + enableViewTransitions: true, + // ... + }, +} +``` From a723e7458c15b0d1db710e73873d063b0d90dccc Mon Sep 17 00:00:00 2001 From: Jairus Joer Date: Sat, 25 Oct 2025 19:18:20 +0200 Subject: [PATCH 4/5] Update docs/features/View Transitions.md Co-authored-by: Aaron Pham --- docs/features/View Transitions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/View Transitions.md b/docs/features/View Transitions.md index 3c47054c4..ace700baa 100644 --- a/docs/features/View Transitions.md +++ b/docs/features/View Transitions.md @@ -42,7 +42,7 @@ startViewTransition(() => { }) ``` -The `startViewTransition` function will automatically use the View Transitions API if it's supported and enabled, or fall back to immediate execution otherwise. +The `startViewTransition` function will automatically use the View Transitions API if the browser is supported, otherwise it will fallback to eager execution. ## Configuration From a295d46018f49d2d25137ba0d936202abe94a285 Mon Sep 17 00:00:00 2001 From: Jairus Joer Date: Tue, 28 Oct 2025 10:19:37 +0100 Subject: [PATCH 5/5] fix: disable view transitions for global graph and adjust graph styles --- quartz.config.ts | 2 +- quartz/components/styles/graph.scss | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/quartz.config.ts b/quartz.config.ts index 70bb89a2e..598884f50 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -11,7 +11,7 @@ const config: QuartzConfig = { pageTitle: "Quartz 4", pageTitleSuffix: "", enableSPA: true, - enableViewTransitions: true, + enableViewTransitions: false, enablePopovers: true, analytics: { provider: "plausible", diff --git a/quartz/components/styles/graph.scss b/quartz/components/styles/graph.scss index 23a4a6f65..53a5fa553 100644 --- a/quartz/components/styles/graph.scss +++ b/quartz/components/styles/graph.scss @@ -1,7 +1,9 @@ @use "../../styles/variables.scss" as *; .graph { - view-transition-name: graph; + &:not(:has(.active)) { + view-transition-name: graph; + } & > h3 { font-size: 1rem; @@ -66,6 +68,7 @@ transform: translate(-50%, -50%); height: 80vh; width: 80vw; + overflow: hidden; @media all and not ($desktop) { width: 90%;