---
description: How I added smooth page-to-page transitions in Astro using the View
  Transitions API. No router, no JavaScript.
keywords:
  - view transitions
  - cross document view transitions
  - astro
  - page transitions css
  - astro view transitions
publishDate: 2025-08-16
tags:
  - Astro
  - CSS
  - Frontend
title: View Transitions in Astro with CSS
---

The [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) has been on my radar for a while. I finally tried it on my [Astro](https://astro.build) site with **Tailwind v4**, and I wanted a hard constraint.

No router.
No extra JavaScript.

Turns out you can get smooth page-to-page transitions with only CSS.

## Why I Tried This

Astro already feels fast. Pages stream as static HTML and navigation is snappy.

But "fast" and "connected" are different feelings.

I wanted a subtle fade that makes navigation feel continuous without looking like an animation demo.

Cross-document view transitions make that possible. The browser can animate between full page loads. If I could enable it globally, the whole site would pick up that polish without hydration.

It worked better than I expected.

## The Core Idea

When you navigate between same-origin pages, the browser can animate the visual change automatically.

You start with one rule:

```css
@view-transition {
  navigation: auto;
}
```

That enables transitions across the site.

After that, you decide what participates by naming elements with `view-transition-name`.

Two layers make this work:

- Global CSS that defines timing and easing
- Consistent HTML structure that names the same elements on every page

## Implementation

There are two moving parts.

CSS defines what the transition looks like.
Markup defines what the browser should "carry over" between pages.

### CSS

This is the full setup I used:

```css
@view-transition {
  navigation: auto;
}

@theme {
  --view-transition: 180ms;
  --view-transition-title: 150ms;
  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
}

::view-transition-group(root) {
  animation-duration: var(--view-transition);
  animation-timing-function: ease-out;
}

::view-transition-group(main) {
  animation-duration: var(--view-transition);
  animation-timing-function: var(--ease);
}

::view-transition-group(title) {
  animation-duration: var(--view-transition-title);
  animation-timing-function: var(--ease);
  animation-delay: 25ms;
}

::view-transition-old(title),
::view-transition-new(title) {
  will-change: transform, opacity;
}

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

  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}
```

What each part does:

- **Enable navigation transitions**
  - `@view-transition { navigation: auto; }`
  - This turns on cross-document transitions for same-origin navigation.

- **Define timing in one place**
  - `--view-transition` is the base duration.
  - `--view-transition-title` is shorter so the title leads.
  - `--ease` gives the motion a soft start and clean finish.

- **Target groups**
  - `root` controls the overall page fade.
  - `main` controls the main content.
  - `title` moves faster and starts slightly earlier.

- **Hint what will animate**
  - `will-change: transform, opacity;`
  - This helps the browser prepare for movement and opacity changes.

- **Respect reduced motion**
  - If a user opts out, do not animate between pages.
  - Also strip animation from all groups to avoid partial motion.

The result is subtle. The page fades. The title shifts first. The rest follows.

### Layout

CSS alone is not enough. You need consistent names across pages.

That is what `view-transition-name` gives you.

```html
<body>
  <main class="[view-transition-name:main]">
    <h1 class="[view-transition-name:title]">Page Title</h1>
    <slot />
  </main>
</body>
```

Rules I follow:

- Do not name the `body`. The browser treats it as `root`.
- Use one `main` and one `title` per page.
- Names must match exactly on both pages.

When the browser sees the same name on the old and new document, it animates between them instead of snapping.

## What I Learned

A few practical notes after testing:

- **Keep the rule global**
  - `@view-transition` must load on every page, not just a single route.

- **Only same-origin, user-triggered navigation animates**
  - Full reloads and opening a new tab do not count.

- **Names must be unique per page**
  - One element per name per document.

- **Avoid naming overlays**
  - Named elements can clip their contents. Skip dropdowns, tooltips, and menus.

- **Short durations feel best**
  - Around 180ms keeps it snappy.
  - Make the title slightly faster if you want hierarchy.

- **Reduced motion is not optional**
  - Disable navigation transitions and remove group animations.

- **Opt out per element**
  - Use `class="[view-transition-name:none]"` to exclude something.

### A note on nav

I leave nav unnamed.

It fades naturally with the rest of the page, which looks intentional and keeps markup simple.

If you want nav to feel "pinned" during transitions, disable its group:

```css
::view-transition-group(nav) {
  animation: none;
}
```

## Debugging

If nothing animates, do the boring checks first.

1. **Slow it down**
   - If you cannot see it, you cannot debug it.

   ```css
   ::view-transition-group(root) {
     animation-duration: 600ms;
   }
   ```

2. **Use DevTools**
   - In Chrome, open the **Animations** panel.
   - `Command+Shift+P` then "Show Animations".

3. **Confirm the basics**
   - CSS loads globally
   - Navigation is same-origin
   - Names exist on both pages and match exactly

## Timing and Rhythm

Small timing changes matter.

### Lead with the title

```css
::view-transition-group(title) {
  animation-duration: 150ms;
  animation-delay: 0ms;
}

::view-transition-group(main) {
  animation-duration: 180ms;
  animation-delay: 25ms;
}
```

The title finishes first. Your eye tracks it. Then content settles.

### Move together

```css
::view-transition-group(title),
::view-transition-group(main) {
  animation-duration: 180ms;
  animation-delay: 0ms;
}
```

Everything stays synchronized. It feels simpler, but flatter.

Those 25ms gaps decide whether motion feels designed or accidental.

## Final Thoughts

This is one of those features that feels like it should require a framework.

It does not.

Astro gives you fast static navigation. The View Transitions API adds continuity. CSS handles the whole thing.

No router. No scripts. Just better page-to-page feel.

## References

- [MDN: View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API)
- [Chrome: View Transitions Overview](https://developer.chrome.com/docs/web-platform/view-transitions)
- [Chrome DevTools: Animations](https://developer.chrome.com/docs/devtools/animations/)
- [Astro View Transitions Guide](https://docs.astro.build/en/guides/view-transitions)