View Transitions

Astro supports opt-in, per-page, view transitions with just a few lines of code. View transitions update your page content without the browser’s normal, full-page navigation refresh and provide seamless animations between pages.

Astro provides a <ViewTransitions /> routing component that can be added to a single page’s <head> to control page transitions as you navigate away to another page. It provides a lightweight client-side router that intercepts navigation and allows you to customize the transition between pages.

Add this component to a reusable .astro component, such as a common head or layout, for animated page transitions across your entire site (SPA mode).

Astro’s view transitions support is powered by the new View Transitions browser API and also includes:

Adding View Transitions to a Page

Section titled Adding View Transitions to a Page

Opt in to using view transitions on individual pages by importing and adding the <ViewTransitions /> routing component to <head> on every desired page.

src/pages/index.astro
---
import { ViewTransitions } from 'astro:transitions';
---
<html lang="en">
<head>
<title>My Homepage</title>
<ViewTransitions />
</head>
<body>
<h1>Welcome to my website!</h1>
</body>
</html>

Full site view transitions (SPA mode)

Section titled Full site view transitions (SPA mode)

Import and add the <ViewTransitions /> component to your common <head> or shared layout component. Astro will create default page animations based on the similarities between the old and new page, and will also provide fallback behavior for unsupported browsers.

The example below shows adding Astro’s default page navigation animations site-wide, including the default fallback control option for non-supporting browsers, by importing and adding this component to a <CommonHead /> Astro component:

components/CommonHead.astro
---
import { ViewTransitions } from 'astro:transitions';
---
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<ViewTransitions />

No other configuration is necessary to enable Astro’s default client-side navigation!

Use transition directives or override default client-side navigation on individual elements for finer control.

Astro will automatically assign corresponding elements found in both the old page and the new page a shared, unique view-transition-name. This pair of matching elements is inferred by both the type of element and its location in the DOM.

Use optional transition:* directives on page elements in your .astro components for finer control over the page transition behaviour during navigation.

In some cases, you may want or need to identify the corresponding view transition elements yourself. You can specify a name for a pair of elements using the transition:name directive.

old-page.astro
<aside transition:name="hero">
new-page.astro
<aside transition:name="hero">

Note that the provided transition:name value can only be used once on each page. Set this manually when Astro can’t infer a proper name itself, or for more fine control over matching elements.

Added in: astro@2.10.0

You can persist components and HTML elements (instead of replacing them) across page navigations using the transition:persist directive.

For example, the following <video> will continue to play as you navigate to another page that contains the same video element. This works for both forwards and backwards navigation.

components/Video.astro
<video controls="" autoplay="" transition:persist>
<source src="https://ia804502.us.archive.org/33/items/GoldenGa1939_3/GoldenGa1939_3_512kb.mp4" type="video/mp4">
</video>

You can also place the directive on an Astro island (a UI framework component with a client: directive). If that component exists on the next page, the island from the old page with its current state will continue to be displayed, instead of replacing it with the island from the new page.

In the example below, the count will not be reset when navigating back and forth across pages that contain the <Counter /> component with the transition:persist attribute.

components/Header.astro
<Counter client:load transition:persist count={5} />

You can also manually identify corresponding elements if the island/element is in a different component between the two pages.

pages/old-page.astro
<Video controls="" autoplay="" transition:name="media-player" transition:persist />
pages/new-page.astro
<MyVideo controls="" autoplay="" transition:name="media-player" transition:persist />

As a convenient shorthand, transition:persist can alternatively take a transition name as a value.

pages/index.astro
<video controls="" autoplay="" transition:persist="media-player">

Astro comes with a few built-in animations to override the default fade transition. Add the transition:animate directive to individual elements to customize the behavior of specific transitions.

  • fade (default): An opinionated crossfade animation. The old content fades out and the new content fades in.
  • initial: Opt out of Astro’s opinionated crossfade animation and use the browser’s default styling.
  • slide: An animation where the old content slides out to the left and new content slides in from the right. On backwards navigation, the animations are the opposite.
  • none: Disable the browser’s default animations. Use on a page’s <html> element to disable the default fade for every element on the page.

Combine directives for full control over your page animation. Set a page default on the <html> element, and override on any individual elements as desired.

The example below produces a slide animation for the body content while disabling the browser’s default fade animation for the rest of the page:

---
import { CommonHead } from '../components/CommonHead.astro';
---
<html transition:animate="none">
<head>
<CommonHead />
</head>
<body>
<header>
...
</header>
<!-- Override your page default on a single element -->
<main transition:animate="slide">
...
</main>
</body>
</html>

You can customize all aspects of a transition with any CSS animation properties.

To customize a built-in animation, first import the animation from astro:transitions, and then pass in customization options.

The example below customizes the duration of the built-in fade animation:

---
import { fade } from 'astro:transitions';
---
<header transition:animate={fade({ duration: '0.4s' })}>

You can also define your own animations for use with transition:animate by defining both the forwards and backwards behavior, as well as new and old pages, according to the following types:

export interface TransitionAnimation {
name: string; // The name of the keyframe
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 following example shows all the necessary properties to define a custom fade animation:

---
const anim = {
old: {
name: 'fadeIn',
duration: '0.2s',
easing: 'linear',
fillMode: 'forwards',
},
new: {
name: 'fadeOut',
duration: '0.3s',
easing: 'linear',
fillMode: 'backwards',
}
};
const myFade = {
forwards: anim,
backwards: anim,
};
---
<header transition:animate={myFade}> ... </header>

The <ViewTransitions /> router handles navigation by listening to:

  • Clicks on <a> elements.
  • Backwards and forwards navigation events.

The following options allow you to further control when navigation occurs within the router:

Preventing client-side navigation

Section titled Preventing client-side navigation

There are some cases where you cannot navigate via client-side routing since both pages involved must use the <ViewTransitions /> router to prevent a full-page reload. You may also not want client-side routing on every navigation change and would prefer a traditional page navigation on select routes instead.

You can opt out of client-side routing on a per-link basis by adding the data-astro-reload attribute to any <a> or <form> tag. This attribute will override any existing <ViewTransitions /> component and instead trigger a browser refresh during navigation.

The following example shows preventing client-side routing when navigating to an article from the home page only. This still allows you to have animation on shared elements, such as a hero image, when navigating to the same page from an article listing page:

src/pages/index.astro
<a href="/articles/emperor-penguins" data-astro-reload>
src/pages/articles.astro
<a href="/articles/emperor-penguins">

Links with the data-astro-reload attribute will be ignored by the router and a full-page navigation will occur.

You can also trigger client-side navigation via events not normally listened to by the <ViewTransitions /> router using navigate. This function from the astro:transitions/client module can be used in scripts, and in framework components that are hydrated with a client directive.

The following example shows an Astro component that navigates a visitor to another page they select from a menu:

src/components/form.astro
<script>
import { navigate } from 'astro:transitions/client';
// Navigate to the selected option automatically.
document.querySelector('select').onchange = (ev) => {
let href = ev.target.value;
navigate(href);
};
</script>
<select>
<option value="/play">Play</option>
<option value="/blog">Blog</option>
<option value="/about">About</option>
<option value="/contact">Contact</option>
</select>
src/pages/index.astro
---
import Form from "../components/form.astro"
import { ViewTransitions } from "astro:transitions";
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<Form />
</body>
</html>

The following example implements the same using navigate() in a React <Form /> component:

src/components/form.jsx
import React from 'react';
import { navigate } from 'astro:transitions/client';
export default function ClickToNavigate({ to }) {
return <select onChange={(ev) => navigate(e.target.value)}>
<option value="/play">Play</option>
<option value="/blog">Blog</option>
<option value="/about">About</option>
<option value="/contact">Contact</option>
</select>;
}

The <Form /> component can then be rendered on an Astro page that uses the <ViewTransitions /> router, with a client directive:

src/pages/index.astro
---
import Form from "../components/form.jsx"
import { ViewTransitions } from "astro:transitions";
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<Form client:load />
</body>
</html>

The navigate method takes these arguments:

  • href (required) - The new page to navigate to.
  • options - An optional object with the following properties:
    • history: 'push' | 'replace' | 'auto'
      • 'push': the router will use history.pushState to create a new entry in the browser history.
      • 'replace': the router will use history.replaceState to update the URL without adding a new entry into navigation.
      • 'auto' (default): the router will attempt history.pushState, but if the URL is not one that can be transitioned to, the current URL will remain with no changes to the browser history.
    • formData: A FormData object for POST requests.

For backward and forward navigation through the browser history, you can combine navigate() with the built-in history.back(), history.forward() and history.go() functions of the browser. If navigate() is called during the server-side render of your component, it has no effect.

Replace entries in the browser history

Section titled Replace entries in the browser history

Normally, each time you navigate, a new entry is written to the browser’s history. This allows navigation between pages using the browser’s back and forward buttons.

The <ViewTransitions /> router allows you to overwrite history entries by adding the data-astro-history attribute to any individual <a> tag.

The data-astro-history attribute can be set to the same three values as the history option of the navigate() function:

data-astro-history: 'push' | 'replace' | 'auto'

  • 'push': the router will use history.pushState to create a new entry in the browser history.
  • 'replace': the router will use history.replaceState to update the URL without adding a new entry into navigation.
  • 'auto' (default): the router will attempt history.pushState, but if the URL is not one that can be transitioned to, the current URL will remain with no changes to the browser history.

The following example navigates to the /main page but does not add a new entry to the browsing history. Instead, it reuses the current entry in the history (/confirmation) and overwrites it.

/confirmation.astro
<a href="/main" data-astro-history="replace">

This has the effect that if you go back from the /main page, the browser will not display the /confirmation page, but the page before it.

Added in: astro@3.5.0

By default, the <ViewTransitions /> router only transitions on navigation triggered by <a> clicks. You can also allow transitions on form submissions by adding the handleForms option on the router.

src/pages/index.astro
---
import { ViewTransitions } from "astro:transitions";
---
<html>
<head>
<ViewTransitions handleForms />
</head>
<body>
<!-- stuff here -->
</body>
</html>

With this option the router will trigger in-page transitions from <form> elements, supporting both GET and POST requests. You can opt out of router transitions on any individual form using the data-astro-reload attribute:

src/components/Form.astro
<form action="/contact" data-astro-reload>
<!-- -->
</form>

The <ViewTransitions /> router works best in browsers that support View Transitions (i.e. Chromium browsers), but also includes default fallback support for other browsers. Even if the browser does not support the View Transitions API, Astro will still provide in-browser navigation using one of the fallback options for a comparable experience.

You can override Astro’s default fallback support by adding a fallback property on the <ViewTransitions /> component and setting it to swap or none:

  • animate (default, recommended) - Astro will simulate view transitions using custom attributes before updating page content.
  • swap - Astro will not attempt to animate the page. Instead, the old page will be immediately replaced by the new one.
  • none - Astro will not do any animated page transitions at all. Instead, you will get full page navigation in non-supporting browsers.
---
import { ViewTransitions } from 'astro:transitions';
---
<title>My site</title>
<ViewTransitions fallback="swap">

Client-side navigation process

Section titled Client-side navigation process

When using the <ViewTransitions /> router, the following steps occur to produce Astro’s client-side navigation:

  1. A visitor to your site triggers navigation by any of the following actions:

    • Clicking an <a> tag linking internally to another page on your site.
    • Clicking the back button.
    • Clicking the forward button.
  2. The router starts fetching the next page.

  3. The router adds the data-astro-transition attribute to the HTML element with a value of 'forward' or 'back' as appropriate.

  4. The router calls document.startViewTransition. This triggers the browser’s own view transition process. Importantly, the browser screenshots the current state of the page.

  5. Inside the startViewTransition callback, the router performs a swap, which consists of the following sequence of events:

    • The contents of the <head> are swapped out, with some elements kept:

      • Stylesheet DOM nodes are left in if they exist on the new page, to prevent FOUC.
      • Scripts are left in if they exist on the new page.
      • Any other head elements with transition:persist are left in if there is a corresponding element in the new page.
    • The <body> is completely replaced with the new page’s body.

    • Elements marked transition:persist are moved over to the new DOM if they exist on the new page.

    • Scroll position is restored if necessary.

    • The astro:after-swap event is triggered on the document. This is the end of the swap process.

  6. The router waits for any new stylesheets to load before resolving the transition.

  7. The router executes any new scripts added to the page.

  8. The astro:page-load event fires. This is the end of the navigation process.

Script behavior during page navigation

Section titled Script behavior during page navigation

When navigating between pages with the <ViewTransitions /> component, scripts are run in sequential order to match browser behavior.

If you have code that sets up global state, this state will need to take into account that the script might execute more than once. Check for the global state in your <script> tag, and conditionally execute your code where possible:

<script is:inline>
if(!window.SomeGlobal) {
window.SomeGlobal = {} // ....
}
</script>

Module scripts are only ever executed once because the browser keeps track of which modules are already loaded. For these scripts, you do not need to worry about re-execution.

The <ViewTransition /> router fires a number events on the document during navigation. These events provide hooks into the lifecycle of navigation, allowing you to do things like show indicators that a new page is loading, override default behavior, and restore state as navigation is completing.

The navigation process involves a preparation phase, when new content is loaded; a DOM swap phase, where the old page’s content is replaced by the new page’s content; and a completion phase where scripts are executed, loading is reported as completed and clean-up work is carried out.

Astro’s View Transition API lifecycle events in order are:

While some actions can be triggered during any event, some tasks can only be performed during a specific event for best results, such as displaying a loading spinner before preparation or overriding animation pairs before swapping content.

Added in: astro@3.6.0

An event that fires at the beginning of the preparation phase, after navigation has started (e.g. after the user has clicked a link), but before content is loaded.

This event is used:

  • To do something before loading has started, such as showing a loading spinner.
  • To alter loading, such as loading content you’ve defined in a template rather than from the external URL.
  • To change the direction of the navigation (which is usually either forward or backward) for custom animation.

Here is an example of using the astro:before-preparation event to load a spinner before the content is loaded and stop it immediately after loading. Note that using the loader callback in this way allows asynchronous execution of code.

<script is:inline>
document.addEventListener('astro:before-preparation', ev => {
const originalLoader = ev.loader;
ev.loader = async function() {
const { startSpinner } = await import('./spinner.js');
const stop = startSpinner();
await originalLoader();
stop();
};
});
</script>
Added in: astro@3.6.0

An event that fires at the end of the preparation phase, after the new page’s content has been loaded and parsed into a document. This event occurs before the view transitions phase.

This example uses the astro:before-preparation event to start a loading indicator and the astro:after-preparation event to stop it:

<script is:inline>
document.addEventListener('astro:before-preparation', () => {
document.querySelector('#loading').classList.add('show');
});
document.addEventListener('astro:after-preparation', () => {
document.querySelector('#loading').classList.remove('show');
});
</script>

This is a simpler version of loading a spinner than the example shown above: if all of the listener’s code can be executed synchronously, there is no need to hook into the loader’s callback.

Added in: astro@3.6.0

An event that fires before the new document (which is populated during the preparation phase) replaces the current document. This event occurs inside of the view transition, where the user is still seeing a snapshot of the old page.

This event can be used to make changes before the swap occurs. The newDocument property on the event represents the incoming document. Here is an example of ensuring the browser’s light or dark mode preference in localStorage is carried over to the new page:

<script is:inline>
function setDarkMode(document) {
let theme = localStorage.darkMode ? 'dark' : 'light';
document.documentElement.dataset.theme = theme;
}
setDarkMode(document);
document.addEventListener('astro:before-swap', ev => {
// Pass the incoming document to set the theme on it
setDarkMode(ev.newDocument);
});
</script>

The astro:before-swap event can also be used to change the implementation of the swap. The default swap implementation diffs head content, moves persistent elements from the old document to the newDocument, and then replaces the entire body with the body of the new document.

At this point of the lifecycle, you could choose to define your own swap implementation, for example to diff the entire contents of the existing document (which some other routers do):

<script is:inline>
document.addEventListener('astro:before-swap', ev => {
ev.swap = () => {
diff(document, ev.newDocument);
};
});
</script>

An event that fires immediately after the new page replaces the old page. You can listen to this event on the document and trigger actions that will occur before the new page’s DOM elements render and scripts run.

This event, when listened to on the outgoing page, is useful to pass along and restore any state on the DOM that needs to transfer over to the new page.

This is the latest point in the lifecycle where it is still safe to, for example, add a dark mode class name (<html class="dark-mode">), though you may wish to do so in an earlier event.

The astro:after-swap event occurs immediately after the browser history has been updated and the scroll position has been set. Therefore, one use of targeting this event is to override the default scroll restore for history navigation. The following example resets the horizontal and vertical scroll position to the top left corner of the page for each navigation.

document.addEventListener('astro:after-swap',
() => window.scrollTo({ left: 0, top: 0, behavior: 'instant' }))

An event that fires at the end of page navigation, after the new page is visible to the user and blocking styles and scripts are loaded. You can listen to this event on the document.

The <ViewTransitions /> component fires this event both on initial page navigation for a pre-rendered page and on any subsequent navigation, either forwards or backwards.

You can use this event to run code on every page navigation, or only once ever:

<script>
document.addEventListener('astro:page-load', () => {
// This only runs once.
setupStuff();
}, { once: true });
</script>

Enabling client-side routing and animating page transitions both come with accessibility challenges, and Astro aims to make sites opting in to View Transitions as accessible-by-default as possible.

Added in: astro@3.2.0

The <ViewTransitions /> component includes a route announcer for page navigation during client-side routing. No configuration or action is needed to enable this.

Assistive technologies let visitors know that the page has changed by announcing the new page title after navigation. When using server-side routing with traditional full-page browser refreshes, this happens by default after the new page loads. In client-side routing, the <ViewTransitions /> component performs this action.

To add route announcement to client-side routing, the component adds an element to the new page with the aria-live attribute set to assertive. This tells AT (assistive technology) to announce immediately. The component also checks for the following, in priority order, to determine the announcement text:

  • The <title>, if it exists.
  • The first <h1> it finds.
  • The pathname of the page.

We strongly recommend you always include a <title> in each page for accessibility.

Astro’s <ViewTransitions /> component includes a CSS media query that disables all view transition animations, including fallback animation, whenever the prefer-reduced-motion setting is detected. Instead, the browser will simply swap the DOM elements without an animation.

View transitions are no longer behind an experimental flag in Astro v3.0.

If you had not enabled this experimental flag in Astro 2.x, this will not cause any breaking changes to your project. The new View Transitions API has no effect on your existing code.

If you were previously using experimental view transitions, there may be some breaking changes when you upgrade your Astro project from an earlier version.

Please follow the instructions below as appropriate to upgrade an Astro v2.x project configured with experimental.viewTransitions: true to v3.0.

Upgrade from experimental.viewTransitions

Section titled Upgrade from experimental.viewTransitions

If you had previously enabled the experimental flag for view transitions, you will need to update your project for Astro v3.0 which now allows view transitions by default.

Remove experimental.viewTransitions flag

Section titled Remove experimental.viewTransitions flag

Remove the experimental flag:

astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
experimental: {
viewTransitions: true
}
});

The <ViewTransitions /> component has been moved from astro:components to astro:transitions. Update the import source across all occurrences in your project.

src/layouts/BaseLayout.astro
---
import { ViewTransitions } from "astro:components astro:transitions"
---
<html lang="en">
<head>
<title>My Homepage</title>
<ViewTransitions />
</head>
<body>
<h1>Welcome to my website!</h1>
</body>
</html>

Update transition:animate directives

Section titled Update transition:animate directives

Changed: The transition:animate value morph has been renamed to initial. Also, this is no longer the default animation. If no transition:animate directive is specified, your animations will now default to fade.

  1. Rename any morph animations to initial.

    src/components/MyComponent.astro
    <div transition:name="name" transition:animate="morph initial" />
  2. To keep any animations that were previously using morph by default, explicitly add transition:animate="initial"

    src/components/MyComponent.astro
    <div transition:name="name" transition:animate="initial" />
  3. You can safely remove any animations explicitly set to fade. This is now the default behavior:

    src/components/MyComponent.astro
    <div transition:name="name" transition:animate="fade" />

Added: Astro also supports a new transition:animate value, none. This value can be used on a page’s <html> element to disable animated full-page transitions on an entire page. This will only override default animation behavior on page elements without an animation directive. You can still set animations on individual elements, and these specific animations will occur.

  1. You may now disable all default transitions on an individual page, animating only elements that explicitly use a transition:animate directive:

    <html transition:animate="none">
    <head></head>
    <body>
    <h1>Hello world!</h1>
    </body>
    </html>

The event astro:load has been renamed to astro:page-load. Rename all occurrences in your project.

src/components/MyComponent.astro
<script>
document.addEventListener('astro:load astro:page-load', runSetupLogic);
</script>

The event astro:beforeload has been renamed to astro:after-swap. Rename all occurrences in your project.

src/components/MyComponent.astro
<script>
document.addEventListener('astro:beforeload astro:after-swap', setDarkMode);
</script>