Skip to content

Last updated: First published:

Guide: Add View Transitions to Starlight

This guide describes what has to be done to add view transitions to an existing Starlight site using the Starlight support of the 👜 Bag of Tricks ✨.

Otherwise stay tuned for how to add Astro’s <ViewTransitions> support to Starlight and enjoy view transitions on all browsers and thanks to Astro’s simulation even on Firefox and other browsers that do not yet have native support for the View Transition API.

Don’t be afraid that this will turn your beautiful website into a blinking jukebox. This Jotter is just an exaggerated example to show what is possible. If you follow the instructions below, you will be rewarded with a very subtle, SPA-like transition effect. And yes, if you want, you can of course later turn the whole thing into a … blinking jukebox.

The introduction page on The Bag’s view transition support for Starlight defines three categories of features for The Bag’s view transition support. This guide currently covers level 1, the mandatory features.

Enable View Transitions

To enable view transitions, you will extend Starlight’s Head component. You create a replacement component and instruct Starlight to use it as a substitute for its Head1. The replacement component covers all aspects classified as mandatory actions. Components to cover the more optional things are in the works!

Step 1: Install astro-vtbot

The 👜 Bag of Tricks ✨ is available on npm. The package is named astro-vtbot. It can be installed as a package or as an Astro integration. See the installation page for details and differences. Let’s assume you just want to add it as a package:

Terminal window
cd /to/my/starlight/project
npm i -D astro-vtbot

Make sure that you have the latest version installed.

Step 2: Create a Head Component

Create a new component inside your project. The file name does not matter. For this example, we choose ./src/components/starlight/Head.astro. You can copy & paste the content for this file:

./src/components/starlight/Head.astro
---
import type { Props } from '@astrojs/starlight/props';
import StarlightHead from '@astrojs/starlight/components/Head.astro';
import VtbotStarlight from 'astro-vtbot/components/starlight/Base.astro';
---
<VtbotStarlight {...Astro.props}>
<StarlightHead {...Astro.props}><slot /></StarlightHead>
</VtbotStarlight>

Step 3: Use the New Head Component

Now you tell Starlight to replace its Head component with yours. Open the astro.config.mjs2 file from your project directory. Make it look like shown below: Make sure that there is an entry named components in the options of your Starlight integration. It must specify a Head entry with the path of the file you have just created.

astro.config.mjs
import { defineConfig } from 'astro/config';
import starlight from "@astrojs/starlight";
export default defineConfig({
...
integrations: [starlight({
...
components: {
Head: "./src/components/starlight/Head.astro",
...
},
...
})]
})
You already had a different entry for Head: in your astro.config.mjs file?

In this case you have to change the import of StarlightHead in your new Head component. Change the import so that it does not load Starlight’s standard Head, but the component you had in your components mapping before:

./src/components/starlight/Head.astro
---
// The file you had in you components mapping for Head before now goes here:
// You probably might have to change "./src" to "/src" to ensure the component is found
import StarlightHead from '@astrojs/starlight/components/Head.astro';
import StarlightHead from '/src/components/MySpecialHead.astro';
...

Try it out for yourself!

Start your project. No matter what browser you use, you should see that there are no full page loads when navigating. The scroll bar and category toggles do not reset when you go to the next page. The search bar remembers your last entry.

In Chromium browsers, you will see the browser’s standard animations for few transitions, i.e. a very quick fade-over of the complete viewport. Browser without native support for view transition will not yet show animations, but just seamlessly swap pages.

Having no fancy, whole viewport animation on a browser without native view transition support is a good thing here. As the exit and entry animations are required to play in sequence, you can not get a real cross-fade. But see here on how to add a cool animation to the main section.

In the next section, we will extend the view transition effect by adding an animation to the main content area and optimizing the styling.

Configuring View Transitions

You can configure several aspects of the view transition effect in your new Head component.

Animating the Main Section

You can add a transition:animate=... directive to the <VtbotStarlight> component. The Bag will teleport the directive from there to the <main> element of the Starlight page. This is the way to enable view transition animations for the main content area of a Starlight site for all browsers.

How to add animations to the main area
<VtbotStarlight {...Astro.props} transition:animate="fade">
<StarlightHead {...Astro.props}><slot /></StarlightHead>
</VtbotStarlight>

If you want to add further styling to the transition group, you can explicitly name it by accompanying transition:animate with transition:name="...". This way you replace the random name that Astro automatically gereted for you with a chosen name that you can use in CSS rules, see the extended configuration example below.

Fallback Control

What behavior do you want for browsers that do not support view transitions natively? Choose from none, swap or animate. Use the viewTransitionFallback attribute of the <VtbotStarlight> component to tell The Bag what you have chosen:

How to specify the fallback option of <ViewTransitions />
<VtbotStarlight {...Astro.props} viewTransitionsFallback="animate">
<StarlightHead {...Astro.props}><slot /></StarlightHead>
</VtbotStarlight>

This only serves as an example. It is not necessary to set viewTransitionsFallback to animate as this is the default setting anyway.

By the default, the sidebar content will not change on navigation. Only the current page marker will be changed, opened, and scrolled into view. There are several ways how you can change this behavior.

Additional Styling

You can also add a <style is:global> section to the file to style the ::view-transition-* pseudo elements for browsers with native view transition support.

Extended Configuration Example

Let’s revisit our Head component and see how it looks with the extensions described above:

./src/components/starlight/Head.astro
---
import type { Props } from '@astrojs/starlight/props';
import StarlightHead from '@astrojs/starlight/components/Head.astro';
import VtbotStarlight from 'astro-vtbot/components/starlight/Base.astro';
import LoadingIndicator from 'astro-vtbot/components/LoadingIndicator.astro';
import ProgressBar from "astro-vtbot/components/ProgressBar.astro";
// import one of Astro's predefined animation functions,
// see https://docs.astro.build/en/guides/view-transitions/#built-in-animation-directives
import { slide } from 'astro:transitions';
---
{/* Define a view transition animation for `<main>`*/}
<VtbotStarlight
{...Astro.props}
transition:name="main" transition:animate={slide({ duration: 150 })}>
<StarlightHead {...Astro.props}><slot /></StarlightHead>
</VtbotStarlight>
{/* update this one to whatever image you want to show as a loading indicator */}
<LoadingIndicator top="80px" right="16px" src="/favicon.svg" />
{/* As an alterative to the LoadingIndicator: */}
<ProgressBar />
<style is:global>
.swup-progress-bar {
background: linear-gradient(
to bottom,
var(--sl-color-accent),
var(--sl-color-accent-high),
var(--sl-color-accent)
);
height: 5px;
}</style>
<style is:global>
/* Slow down Chrome's default animation */
::view-transition-group(root) {
animation-duration: 300ms;
}
/* Do not slide over the sidebars */
::view-transition-group(main) {
overflow: hidden;
}
/* For non-native-view-transition browsers */
.main-pane {
overflow: hidden;
}
/* let title headings morph into each other */
main h1 {
view-transition-name: title-heading;
}
::view-transition-group(title-heading) {
animation-duration: 0.3s;
}
</style>

Optional Features

This section shows how to add features from the optional feature list. This has just two entries right now but will soon grow further.

Loading indicator

Independent of the Starlight support, The Bag provides loading indicators for you to choose from. The above example contains

  • the indicator that makes your favicon flash happily in the top right corner during loading (<LoadingIndicator />)
  • as well as the <ProgressBar /> with some basic styling.

Choose one of those.

Eliminate Pseudo-Scrolling

The default morph animations might look irritating when applied to elements that are larger than the current viewport. This is typical the case if you add an animation to your main area. There is a whole section on the effect and possible solution in this Jotter.

If you just want to get rid of it, add the following to your Head component:

/src/components/starlight/Head.astro
---
//...
import PageOffset from 'astro-vtbot/components/PageOffset.astro';
---
...
<PageOffset name="main" />

For details on the PageOffset component see its description.

Page Order Directions

In the overview, I teased that it might be nice if the direction of the view transition would depend on the page order. Normally, whenever you click on a link, Astro plays a forward animation and when you hit the browser back key, you see a backward animation.

  • If you have previous and next page links on the bottom of your page, clicking them starts forward animations.
  • Clicking page 1, page 2, page 1 in the sidebar in this order gives you three forward animations.

This would not make a difference if you use the default fade animation as it looks the same forwards and backwards. But the default slide animation for example has a direction. And maybe your special Starlight animation has one, too.

The Bag has a component called <PageOrder /> that you can add to your Head component. It automatically changes the order of normal navigation and history traversals. If you navigate to a page that is further down in the sidebar, that will be a forward navigation. If you navigate to a page further up, this is back. If not both, the from and the to page are found in the sidebar, direction retains its original value.

/src/components/starlight/Head.astro
---
//...
import PageOrder from 'astro-vtbot/components/starlight/PageOrder.astro';
---
...
<PageOrder />

All Astro animations support forward and backward out of the box. But what, if you click on a link to your current page? Then the <PageOrder /> component sets a third direction called stay. The default animations interpret unknown directions as forward. This is usually fine and you are all set!

If you want to add a special animation for your the stay direction, …

… you can do so by adding CSS for it:

/src/components/starlight/Head.astro
...
<style is:global>
@keyframes stay-refresh {
0% { opacity: 1; }
1% { opacity: 1; transform: translateY(0px); }
99% { opacity: 1; transform: translateY(100px); }
100% { opacity: 0; }
}
[data-astro-transition='stay']::view-transition-old(main) {
animation: stay-refresh 0.2s ease-in-out both;
}
[data-astro-transition='stay']::view-transition-new(main) {
animation: stay-refresh 0.1s 0.2s reverse ease-in-out both;
}
[data-astro-transition-fallback='old'][data-astro-transition='stay'] main {
animation: stay-refresh 0.2s ease-in-out both;
}
[data-astro-transition-fallback='new'][data-astro-transition='stay'] main {
animation: stay-refresh 0.1s reverse ease-in-out both;
}
</style>

Another page in the Jotter explains why you need four definitions and what purpose they serve. The rules with ::view-transition-...(main) address the transition named “main” for browsers that do have native view transition support. The last two rules address the <main> element for browsers that don’t.

Refrain from merging rules with pseudo-elements with rules without pseudo-elements. Browsers treat unknown elements or properties as errors and discard the entire rule, even if they could understand half of the selector.

Additional Morph Effects

Now it’s getting funny! I made a component that I could use to make the headings of the pages stand out during view transitions. It is called AutoNameSelected.

Without any parameters it selects all headings on all pages and assigns the view transition names vtbot-hx-0, vtbot-hx-1, …. This has the effect that the n-th heading on the current page and the n-th heading on the next page from a view-transition-group. The headings are cut out of the page context and the old heading morphs into the new heading when navigating.

You see this effect on the Jotter pages.

After I added some configuration options to the component, I saw much more potential. Have a look at the image gallery demo, which uses AutoNameSelected to shuffle image tiles around. Or take a look at the table of contents to the right which has a similar effect. Here is how that is done:

/src/components/starlight/Head.astro
---
//...
import AutoNameSelected from 'astro-vtbot/components/AutoNameSelected.astro';
---
...
<AutoNameSelected selector="starlight-toc li" prefix="vtbot-toc" shuffle={true} />

Event though this component was crafted as part of The Bag’s Stalivht support, it is a general purpose component that can well be used beyond Astro.

Customize the Sidebar

In case you want to change the default behavior of the sidebar, the 👜 Bag of Tricks ✨ offers some switches and utility functions.

Replace the Sidebar Content

You can have the content of the sidebar replaced on each navigation. To do this, add the replaceSidebarContent attribute to the VtbotStarlight tag in your Head component.

/src/components/starlight/Head.astro
<VtbotStarlight {...Astro.props} replaceSidebarContent>

This will swap in the sidebar content from the next page when you navigate. This allows you to have different sidebar content on your pages. It also allows you to control the state of the categories, i.e. whether they are shown open or collapsed, with your static page content. Auto-opening and auto-scrolling to the current page marker is not disabled by this switch.

Retain the Current Page Marker

If you want to take control over the current page marker yourself, you can switch of the default update by adding the retainCurrentPageMarker attribute to the VtbotStarlight tag in your Head component. This will also disable auto-opening and auto-scrolling to the current page marker.

/src/components/starlight/Head.astro
<VtbotStarlight {...Astro.props} retainCurrentPageMarker>

On navigation, this leaves the current page marker as is. If you already set the replaceSidebarContent attribute, you do not need retainCurrentPageMarker. To assist you in updating the page marker yourself, The Bag offers these utility functions:

astro-vtbot/components/starlight/utils.ts
// Return the <a> element inside the sidebar that best fits the URL parameter
export function sidebarEntry(url: URL): HTMLAnchorElement | null {...}
// Clears the current page marker in the sidebar without setting a new one
export function clearCurrentPageMarker(): void {...}
// Clears the current page marker and then sets it to the entry that best fits the URL
export function updateCurrentPageMarker(url: URL): void {...}
// Opens all nested categories of a sidebar entry. Without url parameter, opens the entry of the current page marker. With url parameter opens the corresponding sidebarEntry(url). You can set scrollIntoView to false to prevent the entry to be scrolled into view.
export function openCategory(url?: URL, scrollIntoView = true): void {...}

Here best fit in general means exact match, but if there is none, it also looks for the longest common prefix.

To use the utility functions, import one ore more from astro-vtbot/components/starlight/utils:

MySidebarAnimations.astro
---
---
<script>
import { clearCurrentPageMarker, updateCurrentPageMarker, sidebarEntry } from 'astro-vtbot/components/starlight/utils';
...
</script>

Re-initialize Starlight Pages

For most Starlight sites, the following topic is not relevant. We talk about an issue with re-initialization of Starlight pages when there are view transitions that originate from outside the Starlight pages. To be affected by this problem you must fulfill the following conditions:

  • Under the same origin (= protocol, hostname, port): You have additional pages that are not part of your current Starlight content, or yiu even have a second Starlight site.
  • Those pages also have view transitions enabled.

The issue here is that the Starlight pages are perfectly initialized on the first load, which is a full page load. Later navigation within the Starlight realm is also fine, as we use the <ReplacementSwap /> component to keep the app state up to date. But view transitions that come from outside the Starlight’s realm are soft reloads that cannot properly re-initialize the app state.

The Bag’s website has this structure. If that also sounds like your setup, use the <BorderControl /> component to define and protect your Starlight realm by forcing full page loads on incoming view transitions.

Here is the setting used by the Jotter to protect it from incoming view transitions from demos or reusable components.

/src/components/starlight/Head.astro
---
//...
import BorderControl from 'astro-vtbot/components/BorderControl.astro';
---
...
<BorderControl fence={{ inside: ['/jotter/'] }} />

Make Some Noise

I apologize if the sounds of the jotter bother you, but I just couldn’t resist triggering something other than animations through the life cycle events of the astro view transitions. Maybe you have a more subtle sound file than the one the jotter uses. If you want to try it out, it’s really easy:

/src/components/starlight/Head.astro
---
//...
import SwapSound from 'astro-vtbot/components/SwapSound.astro';
---
...
<SwapSound src="/large-steampunk-factory-machine-188048.mp3" />

For further details see the description of the <SwapSound /> component.

Custom Combo Options

What follows is a yet more extended version of the extended configuration example, but without the styling examples. It is not meant to be copied over into your Head but rather as a showcase what optional components you might mix and match in your own Head component. The section with the optional components is highlighted.

./src/components/starlight/Head.astro
---
import type { Props } from '@astrojs/starlight/props';
import StarlightHead from '@astrojs/starlight/components/Head.astro';
import VtbotStarlight from 'astro-vtbot/components/starlight/Base.astro';
import LoadingIndicator from 'astro-vtbot/components/LoadingIndicator.astro';
import ProgressBar from 'astro-vtbot/components/ProgressBar.astro';
import PageOffset from 'astro-vtbot/components/PageOffset.astro';
import PageOrder from 'astro-vtbot/components/starlight/PageOrder.astro';
import AutoNameSelected from 'astro-vtbot/components/AutoNameSelected.astro';
import SwapSound from 'astro-vtbot/components/SwapSound.astro';
import BorderControl from 'astro-vtbot/components/BorderControl.astro';
---
{/* Define a view transition animation for `<main>`*/}
<VtbotStarlight {...Astro.props} transition:name="main" transition:animate="slide">
<StarlightHead {...Astro.props}><slot /></StarlightHead>
</VtbotStarlight>
{/* Customize what image you want to show where */}
<LoadingIndicator top="80px" right="16px" src="/favicon.svg" />
{/* As an alterative to the LoadingIndicator: */}
<ProgressBar />
<style is:global>
.swup-progress-bar {
background: linear-gradient(
to bottom,
var(--sl-color-accent),
var(--sl-color-accent-high),
var(--sl-color-accent)
);
height: 5px;
}
</style>
{/* Decent transitions for scrolled down pages */}
<PageOffset name="main" />
{/* Make animation direction based on the order of the pages in the sidebar */}
<PageOrder />
{/* Without further configuration, make headings stand out during view transitions.*/}
{/* Can be used to declaratively add view transition names,*/}
{/* which opens a door to all kinds of funny thing. */}
<AutoNameSelected />
{/* If you have other pages in this project,*/}
{/* protect the starlight part from incoming view transitions */}
<BorderControl fence={{ inside: ['/jotter/', '/blog/'] }} />
{/* If you want to swap the sound of the page transition */}
<SwapSound src="/path/to/sound.mp3" />

Friendly Neighbor

Currently, The Bag’s support for view transitions is aware of and compatible with the following plugins you might use with your starlight site: *

I plan to continue to support compatibility with these packages in future versions of the 👜 Bag of Tricks ✨.

Known Issues

There are some facts that you should be aware of. None of them is a real obstacle on using The Bag’s Starlight support.

View Transitions into Your Starlight Site from the Outside

If you link to a page of your Starlight site from the outside, you should not use view transitions, but do a full page load instead. This way, your Starlight site will be properly initialized This is also important if you create a link between two Starlight sites.

The website of The Bag automatically intercepts all links that return to the /jotter/ and does a full page load. The Bag will soon offer a reusable component to facilitate that.

Not tested with arbitrary Starlight options

Starlight supports a lot of different config options. The Bags view transition support is so far only tested with a few typical combinations.

Multilingual Starlight sites force a complete reload of the page if you select a different language. This is a good thing, as the app frame changes completely and has to be reloaded anyway.

As always, feedback is very welcome!

I look forward to your feedback! Did it work for you? What were the biggest hurdles? What are you looking for?

Get in touch on Discord or discuss on the 👜 Bag of Tricks ✨‘s github page. And of course I would be more than happy if you would like to sponsor my projects! Made with 💖👜✨!

Footnotes

  1. Here is a pointer to background information on how overriding components works.

  2. Assuming .mjs as the extension here but .ts would also be fine, of course!