From de33ab6dda501c288d453b59827f26e9ae36671d Mon Sep 17 00:00:00 2001
From: Jairus Joer
Date: Sat, 25 Oct 2025 15:42:09 +0200
Subject: [PATCH] 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;
}