Last updated: First published:
Practitioners' Guide
A guide for practitioners on View Transitions
There are 5 events now? How do I choose the right one?
The events are assigned to five positions in the transition process: The beginning and end of the preparation phase, the beginning and end of the swap phase and during the completion phase. Three of them (astro:after-preparation
, astro:after-swap
, astro:page-load
) are used to notify your code that a certain point in the processing has been reached. The other two events (astro:before-preparation
and astro:before-swap
) have additional properties and functions to control the transition process.
While some things can be done in any event, some tasks can only be performed in a specific event, see details below.
I want to have different behavior for different links on the page
You can use the sourceElement
property of the astro:before-*
events to access the anchor element, button or form that triggered the navigation. There you can test for additional information like presents of data-
attributes or style-classes. See demo.
I want to show some “loading…” indicator
Use the event listener of the astro:before-preparation
event to enable the indicator. Hide the indicator in the event listener for the astro:after-preparation
event. This ensure that the indicator is removed before the view transition starts and is not part of the initial screenshot. No need to set loader callback or modify event properties. See demo.
I want to add my own custom animation
Await the ready
promise of the viewTransition
object. Or have your animation triggered by the insertion of the pseudo elements of the view transition API, see details here↗. See demo.
I want to prefetch images that are needed for the target page
View transitions from thumbnails to large images in target files look very poor if the target image is not available when the transition starts. For better results, preload the images before the view transition begins.
Define a new loader
function for the astro:before-preparation
event. Call the original loader
function to do the heavy lifting and get the content of the next page in the newDocument
property. Find the images you want to preload and load them into the cache. Use Promise.all()
to await
that all images are ready. See demo.
TIP: An alternative approach for opaque images is to replace the default cross-over animation with an always visible thumbnail image that gets resized while it moves to the destination.
I want to optimize the number of objects taking part in a transition
In the listener for the astro:after-preparation
event you have access to the current and the future page content. At that point you could remove animation pairs that would otherwise lead to an ugly fly through when part of them are outside the current view port and others are not. You could also introduce new pairs right before the view transition to get some last minute optimization on the ::view-transition-group
effect. (no demo yet)
I want to use a loader that can output percentage events during loading
Yes, you can do that by overriding the loader
property of the astro:before-preparation
event. You should take a look at the current source code and copy most of it, as it has already undergone some bug fixes to handle corner cases. That said, it might be far less attractive than it seems at first glance. Streaming doesn’t know the length of the content in advance. Hosting platforms usually remove the “content-length” header when they compress the response. Static HTML files are usually small and are often loaded with the first chunk of data. (probably never demo’ed)
The default swapping of view transitions resets iframes and animations
Define your own swap algorithm: overwrite the swap
property in the astro:before-swap
event. Instead of throwing the newDocument
on the old one, do a diff of the two structures and only change the bare minimum of the existing DOM to finally reflect the desired outcome. See demo.
Synchronous vs. asynchronous code
Do all event listeners and callbacks have to be synchronous?
All event listeners should only execute synchronous code. The reason for this is that EventTarget.dispatchEvent()
cannot be await
ed for. Another restriction results from the way view transitions work. While the browser executes the code between astro:before-swap
and astro:after-swap
, the user interface is frozen. The browser also enforces a strict timeout of a few seconds to ensure that this freeze does not last too long. For this reason, the swap
callback of the astro:before-swap
event is not await
ed for.
You can use asynchronous code in the event listeners and callbacks, but the processing would not wait for that code to complete and it would actually be executed in parallel with the view transition. So this is clearly not recommended.
I need to execute and await some asynchronous code during the transition
The only way to run asynchronous code during transitions and wait for it to complete is to run it via the loader
hook of the astro:before-preparation
event. With this hook, you can load files from the net, compile Rust into WASM, call ChatGPT for help, or perform any other time-consuming preparation your transition requires.
Tips & Tricks
Common Pitfalls
- My event listener is not called
Make sure that you have added the event listener to the
document
object and not to thewindow
object.Check the spelling of the event name. You might also use predefined constants (e.g.
import { TRANSITION_BEFORE_SWAP } from 'astro:transitions/client';
)Make sure that the handler is installed before the event is triggered: For example, if you install a handler for
astro:after-swap
in an inline script, this script will only be executed after the event has been fired. Listeners forastro:page-load
can also be installed too late, e.g. if they are contained in atype="text/partytown"
script, as partytown deferes the execution of scripts to web workers.- My event listener is called too often
If you navigate from a page without an inline script to a page with an inline script, this script is executed between
astro:after-swap
andastro_page-load
. If this script registers an event listener and you now switch back and forth between these two pages, the handler will be installed again and again.It is best to install event listeners in module scripts, as they are only executed once, even if they are contained on several pages.
- Checking
window.location.pathname
does not work as expected For normal navigation,
window.location.pathname
contains the page on which the navigation begins. However, when navigating through the history (“Back/Forward” button of the browser), it contains the target page of the navigation. To reliably check where you are and where you are going, useevent.from
andevent.to
- The view transition doesn’t honor my style sheets
In browsers with native view transition support, the view transition starts after the DOM has been updated, i.e. after the new page has been swapped in. This means that the style sheet that controls the view transition originate from the page you navigate to and not from the one you leave.
Astro’s simulated view transitions for browsers that do not support them natively runs the “old” animation on the old page, then swaps the DOM, and then runs the “new” animation on the new page. Thus the CSS to control the “old” animation must be on the old page and the “new” animations CCS must be one the new page.
transition:name
only works in .astro
files?Yes. Alternatively, you can define the view-transition-name
CSS property in a style sheet or style attribute.
But be aware that this only works for browsers with native support for the view transition API.
transition:persist
only works in .astro
files?Yes. Alternatively, you can define a custom data property. Always specify an identifier as in data-astro-transition-persist="identifier"
. This works for all browsers.
Page Marker & Guard
When you navigate to a new page in a typical multi-page application, the state of the previous page is completely erased. You start the new page with a clean module loader, all scripts are gone, as are all event listeners.
This is different for view transitions. Since you keep the same document and just swap in new content, your scripts and handlers will slowly accumulate. Astro has taken some precautions to make your life easier: If you switch to a page that defines a script that has already been executed on the previous page, it will not be executed again. This way you don’t add the same event listener twice. You can opt-in to get it re-executed in this case by adding the data-astro-rerun
attribute to the script tag.
But if you have a good idea for an event listener that helps you transition from page A to page B and another one for the transition from B to C, you have two listeners. If you do not explicitly remove it, the A-B listener is still active when you switch from B to C.
Only when you explicitly reload a page without view transitions does the browser forget the listeners.
Removing and reinstalling event listeners is tedious, and even if your website uses several, their number will be small. There is a simple pattern that has worked well for websites with multiple listeners:
- define your handler as an
.astro
component, which should be used in the<head>
in the same way as theViewTransition/>
element. - insert a
<meta/>
element with your marker (this is the reason why our component should be used in the<head>
) - define a function
enabled()
in the script part that checks if the<meta/>
with your marker string is present. - call the
enabled()
function in your event listener to ensure that it is only executed on the pages you intended.
Alternatively, you can also check the event.from
and event.to
values at the beginning of the listener to see whether you want to apply your listener for this transition.
TypeScript
All event listeners have a single parameter of type
Event
. This type does not know the special properties that aTransitionBeforePreparationEvent
or aTransitionBeforeSwapEvent
has. For type-safe access to these properties, you can use the provided functionsisTransitionBeforePreparationEvent
andisTransitionBeforeSwapEvent
: