Skip to content

Last updated: First published:

Add Astro's View Transitions to Starlight for that SPA-like Look & Feel

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 ✨.

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.

I’m currently working on making the optional features available as reusable components as well and will then extend this guide.

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
1
cd /to/my/starlight/project
2
npm i -D astro-vtbot

Make sure that you have the latest version installed.

Step 2: Create the New 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
1
---
2
import type { Props } from '@astrojs/starlight/props';
3
import StarlightHead from '@astrojs/starlight/components/Head.astro';
4
import VtbotStarlight from 'astro-vtbot/components/starlight/Base.astro';
5
---
6
7
<VtbotStarlight {...Astro.props}>
8
<StarlightHead {...Astro.props}><slot /></StarlightHead>
9
</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
1
import { defineConfig } from 'astro/config';
2
import starlight from "@astrojs/starlight";
3
4
export default defineConfig({
5
...
6
integrations: [starlight({
7
...
8
components: {
9
Head: "./src/components/starlight/Head.astro",
10
...
11
},
12
...
13
})]
14
})
You already had a different entry for Head: in your astro.config.njs 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
1
---
2
// The file you had in you components mapping for Head before now goes here:
3
// You probably might have to change "./src" to "/src" to ensure the component is found
4
import StarlightHead from '@astrojs/starlight/components/Head.astro';
5
import StarlightHead from '/src/components/MySpecialHead.astro';
6
...

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
1
<VtbotStarlight {...Astro.props} transition:animate="fade">
2
<StarlightHead {...Astro.props}><slot /></StarlightHead>
3
</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/>
1
<VtbotStarlight {...Astro.props} viewTransitionsFallback="animate">
2
<StarlightHead {...Astro.props}><slot /></StarlightHead>
3
</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
1
---
2
import type { Props } from '@astrojs/starlight/props';
3
import StarlightHead from '@astrojs/starlight/components/Head.astro';
4
import VtbotStarlight from 'astro-vtbot/components/starlight/Base.astro';
5
import LoadingIndicator from 'astro-vtbot/components/LoadingIndicator.astro';
6
// import one of Astro's predefined animation functions,
7
// see https://docs.astro.build/en/guides/view-transitions/#built-in-animation-directives
8
import { slide } from 'astro:transitions';
9
---
10
11
{/* Define a view transition animation for `<main>`*/}
12
<VtbotStarlight
13
{...Astro.props}
14
transition:name="main" transition:animate={slide({ duration: 150 })}>
15
<StarlightHead {...Astro.props}><slot /></StarlightHead>
16
</VtbotStarlight>
17
18
{/* update this one to whatever image you want to show as a loading indicator */}
19
<LoadingIndicator top="80px" right="16px" src="/favicon.svg" />
20
21
<style is:global>
22
/* Slow down Chrome's default animation */
23
::view-transition-group(root) {
24
animation-duration: 300ms;
25
}
26
27
/* Do not slide over the sidebars */
28
::view-transition-group(main) {
29
overflow: hidden;
30
}
31
/* For non-native-view-transition browsers */
32
.main-pane {
33
overflow: hidden;
34
}
35
36
/* let title headings morph into each other */
37
main h1 {
38
view-transition-name: title-heading;
39
}
40
::view-transition-group(title-heading) {
41
animation-duration: 0.3s;
42
}
43
</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 already contains the indicator that makes your favicon flash happily in the top right corner during loading.

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
1
---
2
//...
3
import PageOffset from 'astro-vtbot/components/PageOffset.astro';
4
---
5
...
6
<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
1
---
2
//...
3
import PageOrder from 'astro-vtbot/components/starlight/PageOrder.astro';
4
---
5
...
6
<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
1
...
2
<style is:global>
3
@keyframes stay-refresh {
4
0% { opacity: 1; }
5
1% { opacity: 1; transform: translateY(0px); }
6
99% { opacity: 1; transform: translateY(100px); }
7
100% { opacity: 0; }
8
}
9
[data-astro-transition='stay']::view-transition-old(main) {
10
animation: stay-refresh 0.2s ease-in-out both;
11
}
12
[data-astro-transition='stay']::view-transition-new(main) {
13
animation: stay-refresh 0.1s 0.2s reverse ease-in-out both;
14
}
15
[data-astro-transition-fallback='old'][data-astro-transition='stay'] main {
16
animation: stay-refresh 0.2s ease-in-out both;
17
}
18
[data-astro-transition-fallback='new'][data-astro-transition='stay'] main {
19
animation: stay-refresh 0.1s reverse ease-in-out both;
20
}
21
</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.

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
1
<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
1
<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
1
// Return the <a> element inside the sidebar that best fits the URL parameter
2
export function sidebarEntry(url: URL): HTMLAnchorElement | null {...}
3
4
// Clears the current page marker in the sidebar without setting a new one
5
export function clearCurrentPageMarker(): void {...}
6
7
// Clears the current page marker and then sets it to the entry that best fits the URL
8
export function updateCurrentPageMarker(url: URL): void {...}
9
10
// 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.
11
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
1
---
2
---
3
<script>
4
import { clearCurrentPageMarker, updateCurrentPageMarker, sidebarEntry } from 'astro-vtbot/components/starlight/utils';
5
...
6
</script>

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
1
---
2
import type { Props } from '@astrojs/starlight/props';
3
import StarlightHead from '@astrojs/starlight/components/Head.astro';
4
import VtbotStarlight from 'astro-vtbot/components/starlight/Base.astro';
5
import LoadingIndicator from 'astro-vtbot/components/LoadingIndicator.astro';
6
import PageOffset from 'astro-vtbot/components/PageOffset.astro';
7
import PageOrder from 'astro-vtbot/components/starlight/PageOrder.astro';
8
---
9
10
{/* Define a view transition animation for `<main>`*/}
11
<VtbotStarlight {...Astro.props} transition:name="main" transition:animate="slide">
12
<StarlightHead {...Astro.props}><slot /></StarlightHead>
13
</VtbotStarlight>
14
15
{/* Customize what image you want to show where */}
16
<LoadingIndicator top="80px" right="16px" src="/favicon.svg" />
17
18
{/* Get rid of pseudo-scrolling in the main area */}
19
<PageOffset name="main" />
20
21
{/* Make animation direction based on the order of the pages in the sidebar */}
22
<PageOrder />

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 config 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.

Starlight Image Zoom

If you are using the starlight-image-zoom plugin (highly recommended!), make sure you are using the latest version.

How to use transition:persist

The Starlight integration of the 👜 Bag of Tricks ✨ is based on the <ReplacementSwap/> component, which automatically persists everything outside the div.main-frame area. If you are flexible where to place things, just put them somewhere outside the div.main-frame to persist them and you are done.

As of v1.7.8 <ReplacementSwap /> also handles data-transition-persist attributes in the document trees that are swapped in from the new page. You can use this to persist an element inside the div.main-frame area to keep its state on all Starlight pages.

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!