Using the View Transitions API with Astro
Tags: astro, webdev
Published: 2023-10-26 13:52:24

**Written by [Ohans Emmanuel](https://blog.logrocket.com/author/ohansemmanuel/)✏️**

The View Transition API simplifies changing the DOM with an animated transition, and unlocks new transition effects when navigating between pages. The API is supported in Chrome 111+. 

View Transitions unlocks native browser transition effects between pages for Astro 3.0 using new platform APIs. The good part? Astro is the first major web framework to mainstream View Transitions, and this article will get you started with the new API. 

_Jump ahead:_

*   [Getting started with Astro view transitions](#getting-started-astro-view-transitions)
*   [Two ways to automatically include view transitions](#two-ways-automatically-include-view-transitions)
    *   [Option 1: Lone page view transitions](#option-1-lone-page-view-transitions)
    *   [Option 2: Full site view transitions](#option-2-full-site-view-transitions)
*   [Customizing Astro view transitions](#customizing-astro-view-transitions)
    *   [`transition:name`](#transition-name)
    *   [`transition:animate`](#transition-animate)
    *   [Custom animations](#custom-animations)
    *   [`transition:persist`](#transition-persist)
*   [Handling fallback for unsupported browsers](#handling-fallback-unsupported-browsers)

## Getting started with Astro view transitions <a name="getting-started-astro-view-transitions">

Let’s explore Astro view transitions with a starter project. Run the following command:

```bash
git clone https://github.com/understanding-astro/astro-view-transitions-starter.git && cd astro-view-transitions-starter && npm install && npm start
```

This will:

1.  Clone a starter Astro project
2.  Install all relevant dependencies
3.  Start the application

Open the local port (`localhost:4321`) and view the starter application. Make sure to transition between the home page (with the card list) and the main article page, as shown below: <iframe style="border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 2px;" src="https://codesandbox.io/p/sandbox/1-starting-point-xvk99n?embed=1" width="800" height="450" allowfullscreen="allowfullscreen"></iframe>What we have here is a standard navigation between pages in Astro. Let’s now add some view transitions.

## Two ways to automatically include view transitions <a name="two-ways-automatically-include-view-transitions">

Astro view transitions kick in when navigating from one page to another. When you click a link on the origin page, Astro intercepts the click, prevents the default routing behavior, and provides the transition effect from the origin page to the destination page. 

To enable view transitions in Astro, we import and render the `ViewTransitions` component in the `head` of an Astro page. 

`ViewTransitions` is responsible for adding a client script to your origin page that intercepts clicks to other pages and there are two ways to leverage `ViewTransitions`.

### Option 1: Lone page view transitions

If you need to add view transitions to a single page or between specific pages, go ahead and render `ViewTransitions` in the `head` of the specific pages:

```javascript
// some-page.astro 
---
import { ViewTransitions } from "astro:transitions";
---

<head>
  <ViewTransitions />
</head>

// ... 
```

Note that you must add `ViewTransitions` to the `<head>` of both the origin and destination pages:<iframe style="border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 2px;" src="https://codesandbox.io/p/sandbox/2-adding-view-transitions-lone-page-q6q95w?embed=1" width="800" height="450" allowfullscreen="allowfullscreen"></iframe>

### Option 2: Full site view transitions

To add view transitions to every page in your application, render `ViewTransitions` within the `<head>` element rendered in a shared `Layout` component (or similar). 

With our starter project, we may go to `layouts/Main.astro` and render the `ViewTransitions` component within `<head>`, as shown below:

```javascript
// import the view transitions component
import { ViewTransitions } from "astro:transitions";
---

<html lang="en">
  <head>
    ...
    <!-- Render the view transitions component -->
    <ViewTransitions />
  </head>
  ...
</html>
```

Adding `ViewTransitions` as discussed in standalone pages or shared layout components will capture clicks to other pages and provide a default fade transition, as shown below. Refresh the page if you don’t notice the transition right away:<iframe style="border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 2px;" src="https://codesandbox.io/p/sandbox/2-adding-view-transitions-v733m5?embed=1" width="800" height="450" allowfullscreen="allowfullscreen"></iframe>

### Customizing Astro view transitions

Based on the type of DOM element and its location in the DOM, Astro automatically infers corresponding elements in the origin and destination page. For finer control, we may leverage transition directives:

*   `transition:name`
*   `transition:animate`
*   `transition:persist`

### `transition:name` <a name="transition-name">

With `transition:name` we can specifically associate a pair of DOM elements from the origin and destination page. To do this, we provide the same `transition:name` attribute to the origin and destination page elements. This name must be unique across each page. 

Let’s give this a try. In our demo application, we can associate the media elements on the home and overview pages: ![The media elements on the home and overview pages](https://blog.logrocket.com/wp-content/uploads/2023/10/media-elements-home-overview-pages.png) To associate these elements, we can give them the same `transition:name` attribute value. In `pages/blog/[...path].astro`, find the destination element and provide the `transition:name` as shown below:

```javascript
<!-- 📂 pages/blog/[...path].astro -->
---
const { path } = Astro.params;
--- 

<Main>
  <div
    class="bg-white/80 min-w-[100%] min-h-[40vh] rounded-lg transition-all"
    transition:name=`media-image-${path}`
  >
...
</div>
...
</Main>
```

The `path` variable refers to the blog path e.g., `some-blog` or `another-blog`. Now, do the same for the origin page. 

Pass a `mediaTransitionName` prop to `Card`. This will be translated to a `media-image-${path}` attribute on the media element on the origin page:

```javascript
<!-- 📂 pages/index.astro -->

<Card
    to={`/blog/${path`}
    mediaTransitionName={`media-image-${path}`}
/>
```

By doing this, we now have both elements associated and animated accordingly:<iframe style="border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 2px;" src="https://codesandbox.io/p/sandbox/3-transition-name-8c3ghg?embed=1" width="800" height="450" allowfullscreen="allowfullscreen"></iframe>

### `transition:animate` <a name="transition-animate">

`ViewTransitions` always applies a default fade animation, but we can assertively customize the behavior of specific transitions by adding the `transition:animate` directive to individual elements. 

There are four default animations supported via `transition:animate`:

*   `fade` (default): An opinionated crossfade animation where the old content fades out and the new content fades in
*   `initial`: Uses the browser’s default styling
*   `slide`: The old content slides left, new slides right. This is reversed on backwards navigation
*   `none`: Disables the browser’s default animations

Let’s use `slide` on our content elements: ![The content elements on the home and blog pages](https://blog.logrocket.com/wp-content/uploads/2023/10/content-elements-home-blog-pages.png)  The origin page is represented by `pages/index.astro`. Go ahead and add the `transition:animate` directive to the content stripes by passing a `contentTransitionAnimate` prop, as shown below:

```javascript
// 📂 pages/index.astro 

<Card
   to={`/blog/${path}`}
   contentTransitionAnimate="slide"
 />
```

Now, add `transition:animate` to the destination content parent element as shown below:

```javascript
// 📂 pages/blog/[...path].astro

<article class="flex gap-3 flex-col mt-10" transition:animate="slide">
 // ...
</article>
```

We should now have the content slide animation on both forwards and backwards navigation:<iframe style="border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 2px;" src="https://codesandbox.io/p/sandbox/4-transition-animate-cgknyx?embed=1" width="800" height="450" allowfullscreen="allowfullscreen"></iframe>

### Custom animations <a name="custom-animations">

Aside from the default animations, we may also configure custom animations via `transition:animate`. We can customize an existing default animation, such as `fade` or `slide`, or create a new animation altogether. 

Consider the example below that changes the duration of the `slide` animation on the media content:

```javascript
// 📂 pages/blog/[...path].astro
---
import { slide } from "astro:transitions";
---

<article
    class="flex gap-3 flex-col mt-10"
    // 👀 look here
    transition:animate={slide({ duration: "0.4s" })}
  >
 ...
</article>
```

Now, the content slide animation will come in later, as shown below: <iframe style="border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 2px;" src="https://codesandbox.io/p/sandbox/5-custom-animation-1-9m94jh?embed=1" width="800" height="450" allowfullscreen="allowfullscreen"></iframe>How about creating an altogether custom animation? Let’s make a [bounce animation](https://github.com/animate-css/animate.css/blob/main/source/attention_seekers/bounce.css). 

Go to the `layouts/Main.astro` component and add a global style with the keyframes for the bounce animation, as shown below:

```css
// 📂 layouts/Main.astro

<body>
// ...
    <style is:global>
      @keyframes bounce {
        from,
        20%,
        53%,
        to {
          animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
          transform: translate3d(0, 0, 0);
        }

        40%,
        43% {
          animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
          transform: translate3d(0, -30px, 0) scaleY(1.1);
        }

        70% {
          animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
          transform: translate3d(0, -15px, 0) scaleY(1.05);
        }

        80% {
          transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
          transform: translate3d(0, 0, 0) scaleY(0.95);
        }

        90% {
          transform: translate3d(0, -4px, 0) scaleY(1.02);
        }
      }
    </style>
</body>
```

To create a valid animation, we must follow a strict interface similar to the following:

```javascript
export interface TransitionAnimation {
  name: string;
  delay?: number | string;
  duration?: number | string;
  easing?: string;
  fillMode?: string;
  direction?: string;
}

export interface TransitionAnimationPair {
  old: TransitionAnimation | TransitionAnimation[];
  new: TransitionAnimation | TransitionAnimation[];
}

export interface TransitionDirectionalAnimations {
  forwards: TransitionAnimationPair;
  backwards: TransitionAnimationPair;
}
```

The above describes the expected behavior when navigating between old and new pages. It includes forwards and backwards movement directions. 

Let’s put this into practice. Go to the `pages/blog/[...path].astro` page and create the actual `bounce` custom animation in the frontmatter, as shown below:

```javascript
// 📂 pages/blog/[...path].astro
---
//...
const bounceAnimation = {
  old: {
    name: "bounce", // same name as the keyframe in layouts/Main
    duration: "0.7s",
    easing: "linear",
    fillMode: "forwards",
  },
  new: {
    name: "bounce",
    duration: "0.7s",
    easing: "linear",
    fillMode: "backwards",
  },
};

const bounce = {
  forwards: bounceAnimation,
  backwards: bounceAnimation,
};
---
```

Finally, use this animation for the destination page content as shown below:

```javascript
// 📂 pages/blog/[...path].astro
<article class="flex gap-3 flex-col mt-10" transition:animate={bounce}>
//...
</article>
```

Now, navigate from the home page to the blog destination page and watch the content bounce:<iframe style="border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 2px;" src="https://codesandbox.io/p/sandbox/5-custom-animation-2-nj7wcc?embed=1" width="800" height="450" allowfullscreen="allowfullscreen"></iframe>

### `transition:persist` <a name="transition-persist">

Upon navigation, we can use the `transition:persist` directive to persist components and HTML elements across the origin and destination pages. 

For example, if we had a `video` playing on the origin page, we could persist the state of the `video` component, if it’s rendered in the destination page by doing the following:

```html
<video transition:persist>
...
</video>
```

This is the same with Astro Islands. 

Let’s put this into practice. For this, I’ve prepared a basic counter component built in React, called `ReactCounter`. In the demo home page, go ahead and render the `ReactCounter` island as shown below:

```javascript
// 📂 pages/index.astro
---
import { ReactCounter } from "../components/ReactCounter";
---

<Card
  to={`/blog/${path}`}
  mediaTransitionName={`media-image-${path}`}
  contentTransitionAnimate="slide">
  {/** 👀 Render counter island */}

  <ReactCounter client:load />
</Card>
```

This will render the counter within the card media element: ![The Counter island card media element](https://blog.logrocket.com/wp-content/uploads/2023/10/counter-island-card-media-element.png)  Note that clicking the media element updates the counter. To navigate to the blog page, you must click outside the media element, which in our case is the card content. 

Go ahead and also render the counter in the blog destination page as shown below:

```javascript
// 📂 pages/blog/[...path].astro
---
import { ReactCounter } from "../../components/ReactCounter";
--- 

<Main>
  <div
    class="bg-white/80 min-w-[100%] h-[40vh] rounded-lg transition-all"
    transition:name=`media-image-${path}`
  >
    <ReactCounter client:load />
  </div>
  // ...
</Main>
```

This will render the counter in the main blog media element: ![The counter island in the main blog media element](https://blog.logrocket.com/wp-content/uploads/2023/10/counter-island-main-blog-media-element.png)  By default, if we transition between the home page and the blog page, the state of the counter is always reset as shown below: ![The default counter transition behavior](https://blog.logrocket.com/wp-content/uploads/2023/10/default-counter-transition-behavior-1.gif) However, we can change that by adding `transition:persist` to the rendered `ReactCounter` islands and giving the counters a unique `transition:name` to make sure the multiple islands on the home page are treated as distinct elements:

```javascript
// 📂 index.astro
<ReactCounter
  client:load
  transition:persist
  transition:name={`counter-${path}`}
/>

// 📂 pages/blog/[...path].astro

<ReactCounter
  client:load
  transition:persist
  transition:name={`counter-${path}`}
/>
```

Now, navigate from the home page to the blog and back. Note that the counter’s state remains preserved:<iframe style="border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 2px;" src="https://codesandbox.io/p/sandbox/5-custom-animation-2-nj7wcc?embed=1" width="800" height="450" allowfullscreen="allowfullscreen"></iframe>

## Handling fallback for unsupported browsers <a name="handling-fallback-unsupported-browsers">

`<ViewTransitions />` works best for browsers that support view transitions, i.e., Chromium-browsers. However, it also ships support for other browsers. 

For example, at the time of writing, Firefox doesn’t support view transitions, but here’s our demo in Firefox: ![View transitions in Astro using Firefox](https://blog.logrocket.com/wp-content/uploads/2023/10/view-transitions-astro-firefox.gif) Astro still tries to provide a comparable transition experience. If you wish to disable support in other browsers, pass `none` to the `fallback` prop, as shown below:

```javascript
<ViewTransitions fallback="none" />
```

This will trigger a full page reload in non-supporting browsers. To disable a full-page reload and disable animations upon navigation, pass the `swap` prop value as shown below:

```javascript
<ViewTransitions fallback="swap" />
```

## Conclusion

View transitions bring a native feel to the web, and with Astro being the first major web framework to support them, you can start building arguably better transitions today. 

Visit the [Astro view transitions examples GitHub repo](https://github.com/understanding-astro/astro-middleware-examples) for all the examples discussed here.

---

## Get set up with LogRocket's modern error tracking in minutes:

1. Visit https://logrocket.com/signup/ to get an app ID.
2. Install LogRocket via NPM or script tag. `LogRocket.init()` must be called client-side, not server-side.

NPM:
```bash
$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
```
Script Tag:
```javascript
Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
```

3.(Optional) Install plugins for deeper integrations with your stack:
*   Redux middleware
*   ngrx middleware
*   Vuex plugin

[Get started now](https://lp.logrocket.com/blg/signup)