View transitions in Astro: how to turn them on and why mine were off
“Effects with Tailwind” series · 4 of 4
- Build an animated border with Tailwind CSS
- Glassmorphism without wrecking legibility
- Scroll-driven animations
- View transitions in Astro ← you are here
A view transition is what makes the card image, when you open an article from a listing, move to its new position and size instead of disappearing and loading again. It’s a small detail, but it makes navigation feel a lot smoother.
I wrote this article because, going through my own blog, I found seven elements with transitions that had been declared for months, and none of them worked. I explain why, because it’s an easy mistake to make and it gives no warning.
How it works: pairs of names
The mechanism is simple: you give the same name to an element on the source page and to its counterpart on the destination page:
<!-- In the listing -->
<img src="/cover.webp" style="view-transition-name: cover-my-post" />
<!-- In the article -->
<img src="/cover.webp" style="view-transition-name: cover-my-post" />
The browser sees the same name on both pages, understands it’s the same object and animates the difference in position, size and opacity. You don’t have to write the animation.
In Astro you declare it with the transition:name directive, which is more convenient:
<img src={image.url} alt={image.alt} transition:name={`image-${image.url}`} />
My mistake: names without turning transitions on
transition:name only puts the label on; it doesn’t turn anything on.
For a transition to happen, the browser also has to know that navigations should be animated, and that’s a separate piece I didn’t have anywhere in the project. The result was seven transition:name directives generating their CSS correctly and no transitions at all, without a single error in the console.
If you suspect the same is happening to you, you can check quickly from your project root:
grep -rn "ClientRouter\|@view-transition" src/
If that returns nothing and you do use transition:name, you’re in the same spot I was.
There are two ways to turn them on.
Option 1: native CSS, no JavaScript
The most current way is a two-line CSS rule in your global stylesheet:
@view-transition {
navigation: auto;
}
With that, the browser animates navigation between pages on its own. There’s no router, no JavaScript and no added weight: the site is still a normal site with normal links.
That’s why it’s the one I prefer, and the one this blog uses. Its limit is that it depends on the browser supporting cross-document transitions; where there’s no support, links work the same, just without animation. You can check it on Can I use.
Option 2: Astro’s <ClientRouter />
The alternative is Astro’s router, which intercepts links and handles navigation with JavaScript:
---
import { ClientRouter } from "astro:transitions";
---
<html lang="en">
<head>
<ClientRouter />
</head>
<!-- ... -->
</html>
In exchange for the JavaScript it adds, it offers things native CSS doesn’t: it works in more browsers, lets you control animations with transition:animate, and has transition:persist to keep an element alive across pages, useful for an audio player or an open menu.
To choose: if you just want images and titles to carry over from one page to the next, use native CSS. If you need to keep state or control each animation, use ClientRouter.
The second bug: duplicate names
This one is harder to spot.
A view-transition-name has to be unique within a page. If two elements share a name, the browser doesn’t pick one: it cancels the whole transition.
And it was happening to me. On my blog’s front page, the latest post appeared twice: once in the big card at the top and again in the listing below. Both elements generated the same name from the image URL, so the HTML had this:
2 × view-transition-name: image-_2fimages_2fposts_2faccesibilidad_2ewebp
Two occurrences of the same name. Even with transitions turned on, that page would never have animated.
To catch it you have to look at the generated HTML, not the source code, because the duplicate comes from two different components rendering the same post:
npm run build
grep -oE 'view-transition-name: *[^;"]*' dist/blog/index.html | sort | uniq -d
If uniq -d returns anything, those are your cancelled transitions. The command is worth keeping handy.
I fixed it from both sides. First, not repeating the post: the listing component already accepted a prop to exclude the latest one, and using it was enough. Second, I stopped deriving the name from the image URL and took it from the post’s slug, which is unique:
// utils/transitions.ts
export const postSlug = (url?: string) =>
(url ?? "").replace(/\/+$/, "").split("/").pop() ?? "";
The first fix removed the duplicate I had that day, and the second keeps it from coming back. In general, if you generate names from data (a slug, a URL, an id), make sure that data can’t repeat on the same page. Two articles can share a cover image, but not a slug.
Naming well
Three rules that would have saved me all of this:
- Take the name from something stable and unique. The post’s slug is better than the title, and much better than the image URL, because two articles can use the same cover.
- In listings, every card needs a different name. Only the one the user opens will animate, but since you don’t know which one that will be, all of them need a name, and never a repeated one.
- Animate few things. An image and a title per transition are enough; the more elements move, the easier it is for one to fall out of line and the effect to look messy.
Accessibility
As with any motion, you have to respect people who asked for the interface not to move:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
This turns the animation off and makes the page change instant, which is what that person asked for. Navigation keeps working; only the motion goes away.
That’s the end of the series. If you’ve had transition:name in place for a while, it’s worth checking today that transitions are actually turned on: I went months without them and didn’t notice.

