I wanted exactly one thing from astro:transitions: a soft cross-fade between the home page and an article page, so navigating didn’t feel like a hard reload even though, technically, nothing was being kept alive across the navigation. I did not expect this to take a weekend.
Turning it on is the easy part
Dropping <ClientRouter /> into the layout took about thirty seconds and produced a page transition immediately. It also produced a transition on every single element with a shared id, most of which I hadn’t intended — the header wordmark slid instead of cross-fading, and a stray duplicate heading id caused two unrelated elements to morph into each other for one very strange frame.
---
import { ClientRouter } from 'astro:transitions';
---
<ClientRouter />
Scoping it down
The fix was to be deliberate about which elements actually get a transition:name, instead of letting the router infer shared elements from matching ids:
- The article title gets an explicit
transition:nameso it feels continuous between the card and the article page. - Everything else gets
transition:animate="fade"explicitly, or nothing at all, so it doesn’t try to be clever with elements that just happen to share markup. - The header and footer opt out entirely with
transition:persist, since they’re identical across every page and re-animating them is pure waste.
Respecting the setting that matters most
None of this is worth much if it ignores prefers-reduced-motion. Astro’s router respects it by default for its own transitions, but I’d layered a few CSS transitions of my own on top, and those needed an explicit media query:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
A weekend for one cross-fade sounds like a bad trade until I remember what it replaced: a full white flash between every page, which is the kind of thing readers feel even when they can’t articulate why a site feels slower than it measures.