Skip to content

Page transitions without a client-side router

3 min read CSS, performance, accessibility, web platform

A page transition can make a multi-page website feel connected. It does not require turning the site into a single-page app, keeping a JavaScript router in sync, or delaying navigation until an animation finishes. Modern browsers can animate between documents while links still point to real URLs and the server still returns complete HTML.

The key is to treat animation as an enhancement to navigation, not as navigation itself.

Every destination should work with a normal anchor:

<a href="/work/">Work</a>

That preserves browser history, open-in-new-tab behavior, link previews, and navigation when JavaScript is unavailable. Build and test the pages as ordinary documents first. A transition should never be responsible for loading the next page or deciding whether a link works.

Opt in to document transitions

For same-origin pages, cross-document transitions are opted into with a small CSS rule. Both the page being left and the page being entered need to opt in:

@view-transition {
  navigation: auto;
}

The browser can then animate the old and new snapshots during a normal navigation. Browsers that do not support the feature simply perform the usual page load. There is no router to maintain and no JavaScript bundle to download just for the effect.

Start with the default transition. It is easier to judge whether an animation helps when it is not competing with custom timing, easing and choreography.

Connect one meaningful element

If a visual element appears on both pages, give it the same view-transition-name on each. For example, a project thumbnail can expand into the corresponding project page image:

.project-image {
  view-transition-name: project-image;
}

The name must be unique among the rendered elements on a page. If a grid contains many project images, assign a distinct name to the image being navigated from and to, rather than giving every image the same name. If the element is not present on both pages, the browser will transition it as an entering or leaving element instead of matching it as a pair.

Use this sparingly. A shared transition works best when it explains a relationship, such as a list item becoming a detail-page heading or image. Animating every piece of the layout makes the destination harder to read and can make a short navigation feel slower.

Respect reduced motion

Cross-fades and large movements can be uncomfortable for people who have requested less motion. Disable the document transition when that preference is active:

@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }
}

Keep the pages and their content identical in both modes. The preference changes the animation, not the information or route. If you add separate transitions for theme changes or other interactions, check those against the same preference too.

Avoid turning polish into a delay

The destination should render immediately. Avoid adding a loading screen, waiting on unrelated network requests, or running a long animation before the user can interact with the new page. Keep the motion brief, and check the result on a slower device as well as a desktop browser.

It is also worth testing the states that are easy to miss: direct-loading a detail URL, refreshing it, using Back and Forward, opening a link in a new tab, and navigating with reduced motion enabled. If any of those depend on the animation code, the enhancement has become a dependency.

A small launch checklist

  • Every destination is a real URL reached through an ordinary link.
  • Both documents opt in to cross-document transitions.
  • Shared transition names are unique on each page.
  • Reduced-motion preferences disable navigation animation.
  • Unsupported browsers and direct page loads still show complete content.
  • The animation helps identify continuity, rather than obscuring the new page.

That is enough to make a server-rendered site feel more cohesive without giving up the reliability of the web’s simplest navigation model.