zudo-doc
GitHub repository

Type to search...

to open search from anywhere

Dynamic Page Transition

Created Jun 20, 2026Updated Jun 21, 2026Claude

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:

  1. 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.

  2. History handling. The router updates the browser history with pushState so the URL changes and the Back/Forward buttons work exactly as they would with a normal navigation. Pressing Back fires a popstate event, which the router handles by swapping back to the previous page — again without a full reload.

  3. 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-transition

What "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/zudo-doc/page-loading.css.

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:

TokenUsed forFallback
--color-page-loading-overlayThe scrim (overlay background)color-mix(in oklch, var(--color-overlay) 60%, transparent)
--color-overlayBacking colour for the scrim fallback#000 (used only inside the fallback above)
--color-fgSpinner border colour#fff
--color-accentHighlight on the pending link/button(no fallback — the highlight is simply omitted if unset)
--z-index-modalStack level of the overlay100

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.

Revision History

ClaudeCreated: 2026-06-20T14:09:07ZUpdated: 2026-06-21T16:13:42Z

AI Assistant

Ask a question about the documentation.