Dynamic Page Transition
SPA-style soft navigation with a cross-fade, a loading overlay, and preserved chrome between pages.
Overview
By default, zudo-doc navigates between pages without a full browser reload. Instead of fetching a brand-new document and repainting the whole window, it swaps only the page body in place, keeps the surrounding chrome (header, sidebar, footer) untouched, and animates the change with a cross-fade. The result feels like a single-page app (SPA): fast, smooth, and free of the white flash that a hard navigation produces.
This behaviour is called Dynamic page transition, and it is on by default.
Note
Dynamic page transition is purely a presentational enhancement. The site is still a fully static export — every page exists as standalone HTML and works without JavaScript. The transition layer only intercepts in-app link clicks when JS is available; everything degrades to ordinary navigation otherwise.
What It Does
A dynamic navigation combines three pieces:
Same-document body swap. When you click an internal link, the router fetches the target page in the background, then replaces the current page's body content with the new page's content — without tearing down and rebuilding the entire document. The header, sidebar, and footer DOM nodes persist across the navigation, so their scroll position and state are preserved.
History handling. The router updates the browser history with
pushStateso the URL changes and the Back/Forward buttons work exactly as they would with a normal navigation. Pressing Back fires apopstateevent, which the router handles by swapping back to the previous page — again without a full reload.View Transitions API cross-fade. The swap is wrapped in the browser's View Transitions API, which captures a snapshot of the old and new states and animates between them. The non-chrome content cross-fades, while the header, sidebar, and footer are extracted into their own transition layers and held static so they don't flicker.
Info
The chrome elements (<header>, the desktop sidebar, <footer>, and the sidebar-toggle button) are pinned with a stable view-transition-name, which is what lets the cross-fade animate only the article area. When a chrome element exists on one page but not the next (for example, moving from a docs page with a sidebar to a top page without one), that element fades in or out in sync with the content instead of snapping.
The Loading Overlay
While the target page is being fetched, zudo-doc shows a full-screen loading overlay with a centered spinner. It appears when the navigation begins and disappears as soon as the new content is swapped in, giving immediate feedback that a click was registered — especially useful on slower connections where the fetch takes a moment.
The link or button you clicked is also briefly highlighted while its navigation is pending, so it's clear which action is in flight.
Tip
The overlay only shows for the brief window between click and swap. For fast local navigations it may flash by almost instantly — that's expected. Its job is to cover the perceptible delay on slower fetches, not to add an artificial pause.
Reduced Motion
zudo-doc respects the user's prefers-reduced-motion setting. When a visitor has reduced motion enabled at the OS level:
The cross-fade animation is collapsed to an instant swap — the new page appears immediately with no fade or slide.
The spinner stops rotating and is shown as a static dimmed ring instead of an animated one.
The navigation itself still works the same way (same-document swap, history handling); only the motion is removed. This keeps the experience comfortable for users who are sensitive to animation while preserving the performance benefit of soft navigation.
Toggling the Feature
Dynamic page transition is controlled by the dynamicPageTransition setting, and it is enabled by default.
In settings.ts
export const settings = {
// ...
dynamicPageTransition: true,
};Set it to false to disable dynamic transitions entirely.
In the preset generator
When scaffolding a new project with create-zudo-doc, the option is labelled "Dynamic page transition" and is enabled by default. You can control it non-interactively with CLI flags:
# Explicitly enable (this is the default)
create-zudo-doc my-docs --dynamic-page-transition
# Disable — generate a project with plain full-page navigation
create-zudo-doc my-docs --no-dynamic-page-transitionWhat "Off" Means
With dynamic page transition disabled, navigation falls back to plain full-page loads:
Every internal link triggers a normal browser navigation — the document is fetched and the whole page is repainted.
There is no SPA body swap: the header, sidebar, and footer are rebuilt on each navigation rather than persisting.
There is no loading overlay and no cross-fade — you get the browser's default navigation behaviour.
This is a simpler model with zero transition JavaScript. Choose it if you prefer a classic multi-page experience, need to rule out any interaction between the soft-swap router and custom client-side scripts, or simply don't want the animation.
Note
Turning the feature off does not change your content or routes in any way — only how the browser moves between pages. Every page remains a standalone, statically exported HTML document either way.
Wiring the Overlay in a Standalone Layout
If you build your layout with DocLayoutWithDefaults, the loading overlay is already wired for you — there is nothing to do, and you can skip this section. It applies only when you assemble your own layout (without DocLayoutWithDefaults) and want the same overlay behaviour.
The overlay ships from @takazudo/zudo-doc as two pieces you wire yourself: the PageLoadingOverlay component and its stylesheet, @takazudo/.
Mount the component
Import PageLoadingOverlay and render it once per layout, in a body-end slot (the same place you mount your other body-end providers). It is server-rendered with no hydration — it emits a small inline script that self-wires its visibility to the navigation lifecycle (zfb:before-preparation / zfb:after-swap), so there is no island to register and no client:* directive to add.
import { PageLoadingOverlay } from "@takazudo/zudo-doc/page-loading";
// ...inside your layout, at the end of <body>:
<PageLoadingOverlay />;The component takes one optional prop, id, which overrides the DOM id of the overlay element (useful only when more than one overlay could co-exist on a page, such as in tests). Leave it unset for the stable default.
Warning
Mount it once per layout. A second instance would render a duplicate overlay element and a second bootstrap script.
Import the stylesheet
The component renders the overlay markup but not its CSS. Import the shipped stylesheet at the top of your global.css, alongside the other package CSS imports:
@import "@takazudo/zudo-doc/page-loading.css";Warning
The import must appear before any rule statements in your global.css. CSS requires all @import rules to precede other rules — placing it after a selector or @theme block silently drops the import, and the overlay renders unstyled.
Host tokens the stylesheet consumes
The stylesheet reads a handful of host design tokens. All are optional — each has a sensible built-in fallback, so the overlay works on a bare project that defines none of them:
| Token | Used for | Fallback |
|---|---|---|
--color-page-loading-overlay | The scrim (overlay background) | color-mix(in oklch, var(--color-overlay) 60%, transparent) |
--color-overlay | Backing colour for the scrim fallback | #000 (used only inside the fallback above) |
--color-fg | Spinner border colour | #fff |
--color-accent | Highlight on the pending link/button | (no fallback — the highlight is simply omitted if unset) |
--z-index-modal | Stack level of the overlay | 100 |
If your project already defines the standard zudo-doc semantic tokens, the overlay picks them up automatically and matches your theme with no extra work.
Retoning the scrim independently
The scrim now reads its own token, --color-page-loading-overlay, which is decoupled from the lightbox/image-enlarge backdrop (--color-overlay). Set it to retone the loading scrim without touching any other overlay:
@theme {
/* A light, frosted brand scrim for the loading overlay only —
the image lightbox keeps using --color-overlay. */
--color-page-loading-overlay: color-mix(
in oklch,
var(--color-bg) 70%,
transparent
);
}When --color-page-loading-overlay is unset, the stylesheet falls back to a 60%-opacity mix of --color-overlay, so existing projects keep their previous appearance with no change.
Drop any hand-copied overlay rules
Before this stylesheet shipped, standalone consumers had to hand-copy the overlay, spinner, and pending-link rules into their own global.css. If you did this, remove those copied rules when you adopt the @import. Keeping both leaves a duplicate @keyframes page-loading-spin and colliding .page-loading-overlay / .page-loading-spinner declarations, where whichever rule wins is order-dependent and easy to get wrong. The shipped stylesheet is now the single source of truth.