loke.dev
Header image for The Day My HTML Finally Stopped Flickering: A Journey into Cross-Document View Transitions

The Day My HTML Finally Stopped Flickering: A Journey into Cross-Document View Transitions

Unlock seamless, app-like navigation for traditional multi-page websites without the overhead of a JavaScript router or a single-page framework.

· 4 min read

You don’t need a 500kb JavaScript framework to make your website feel like a high-end mobile app. For years, we’ve been told that if we want smooth, cinematic transitions between pages, we have to go the Single Page Application (SPA) route. We signed up for complex routers, state management headaches, and the inevitable "white flash" of death just to avoid a harsh browser reload.

But the browser finally grew up. We can now have seamless, "app-like" transitions between completely separate HTML documents with literally one line of CSS. It’s called Cross-Document View Transitions, and it’s the best thing to happen to the boring old Multi-Page Application (MPA) since the invention of the <a> tag.

The "One Line" Magic

I remember the first time I saw a page slide gracefully into another without a single line of React. I thought I was hallucinating. Here is the magic incantation you need in your global CSS file:

@view-transition {
  navigation: auto;
}

That’s it. By adding this, you’re telling the browser: "Hey, when the user clicks a link to another page on my site, don't just snap to the new content. Take a snapshot of the current page, a snapshot of the next page, and cross-fade them."

It turns the jarring "blink" of a page load into a soft, professional transition. But cross-fades are just the beginning.

Teaching the Browser "Who is Who"

A simple cross-fade is nice, but the real "wow" factor happens when specific elements move across the screen. Imagine a thumbnail on your blog's homepage flying across the page to become the hero image on the actual post.

To do this, we use the view-transition-name property. You just have to give the same name to the element on Page A and the element on Page B.

On your `index.html` (the list page):

.card-image {
  view-transition-name: featured-hero;
}

On your `post.html` (the article page):

.post-hero-image {
  view-transition-name: featured-hero;
}

When you click the link, the browser realizes these two elements share the same identity. Instead of fading them, it literally animates the position and size of the first one until it morphs into the second one. It feels like black magic, but it’s just the browser doing the heavy lifting for you.

Handling the "Wait, Everything is Morphing" Problem

One mistake I made early on was giving the same view-transition-name to every item in a list. Don't do that.

If you have ten blog cards and they all have view-transition-name: card;, the browser gets confused and usually just defaults back to a fade because it doesn't know which card is which. These names must be unique within a single page.

If you’re using a templating engine (like 11ty, Astro, or even PHP), you can generate these names dynamically:

<!-- In your template loop -->
<img 
  src="/path/to/img.jpg" 
  style="view-transition-name: card-{{ post.id }}" 
/>

Customizing the Vibe

By default, the transition is a quick 250ms fade. It’s "safe," but maybe you want something punchier. You can target the transition snapshots using special CSS pseudo-elements: ::view-transition-old() and ::view-transition-new().

Want a slide effect instead of a fade?

/* Slide the old page out to the left and the new one in from the right */
::view-transition-old(root) {
  animation: 0.4s ease-out both slide-out;
}

::view-transition-new(root) {
  animation: 0.4s ease-out both slide-in;
}

@keyframes slide-out {
  from { opacity: 1; }
  to { opacity: 0; transform: translateX(-30px); }
}

@keyframes slide-in {
  from { opacity: 0; transform: translateX(30px); }
  to { opacity: 1; }
}

The "Gotchas" (Because there's always a catch)

Before you go deleting your node_modules folder, there are a few things to keep in mind:

1. Same-Origin Only: This only works for navigations on your own site. If a user clicks a link to an external site, they get the standard "snap."
2. Browser Support: As of right now, this is heavily a Chromium feature (Chrome, Edge, Brave). Safari and Firefox are dragging their feet, but the beauty is that it’s a progressive enhancement. If a user's browser doesn't support it, the site still works perfectly—it just lacks the fancy animation. No harm done.
3. Accessibility Matters: Some people get motion sickness from sliding and zooming elements. Always wrap your custom animations in a media query:

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

Why This Matters

For a long time, the "web" felt clunky compared to native apps. To fix that, we built increasingly complex JavaScript layers that made our sites heavy and fragile.

Cross-Document View Transitions represent a shift back to the "Platform." We're letting the browser handle the UI complexity while we focus on the content. My HTML finally stopped flickering, and honestly? My lighthouse scores have never looked better.

Go ahead, drop @view-transition { navigation: auto; } into your CSS today. Your users' eyes will thank you.