Skip to content

Last updated: First published:

Defining Transitions

This page gives a first overview on how to add transitions to a view transition enabled Starlight site.

Visit the corresponding section in the overview to see where we are in the big picture of The Bag’s Starlight support.

When you follow the initial three steps of the guide, you enabled the default fade animations on the <html> root element for browsers with native view transition support and no animations on other browsers.

Adding directives

To add transitions, you want to use Astro’s transition:* directives.

There are some problems now:

  • The directives must be added to some HTML elements. You do not write HTML. You write Markdown.
  • Ok, you can add HTML elements to *.mdx files. Sadly, the directives only work in *.astro files, not with *.mdx files. But you can use Astro components in *.mdx files, and they can use the directives.
  • You could override more of Starlight’s built-in components.

I would advise against the last point unless you find yourself in a situation where it is really necessary. I think in most cases the element you would choose for a transition group is the main area of the Starlight page anyway, right?

Teleport Magic

Since we don’t want to dig into the guts of Starlight, The Bag provides a special teleport functionality. Simply place your transition:* directives on the <VtbotStarlight> component in your new <Head> component and The Bag will beam them to the <main> element of Starlight’s main content area.

src/components/starlight/Head.astro
{/* Define a view transition animation for `<main>`*/}
<VtbotStarlight
{...Astro.props}
transition:name="main"
transition:animate={slide({ duration: 300 })}
>
<StarlightHead {...Astro.props}><slot /></StarlightHead>
</VtbotStarlight>

Scott me up, Beamy!

If you are interested, this is how that teleporting thing works. You have seen these fragments before:

astro-vtbot/components/starlight/Base.astro
...
export interface Props extends StarlightProps {
...
'data-astro-transition-scope'?: string;
...
}
const { ... 'data-astro-transition-scope': mainTransitionScope ... } = Astro.props;
---
...
{mainTransitionScope && <meta name="vtbot-main-transition-scope" content={mainTransitionScope} />}
...

With one or more transition:* directives on an HTML element, Astro creates a new transition scope and links it to this element using the data-astro-transition-scope attribute.

The creation of the scope also works if the directives are not used on an HTML element, but with an Astro component tag. In this case, the data-astro-transition-scope is created as an entry in the Astro.props object. It is lost if it is not handled explicitly.

The above declaration and code look for the value of the transition scope and embed it into the HTML using a <meta> element, if present. Now we have made the value available to client-side scripts, but it is not yet associated with an HTML element. We can now select any element, especially the <main> element of the content area.

And that is exactly what happens. The value is added as an data-astro-transition-scope attribute to the <main> element in the current document and in the e.newDocument that was just fetched by the loader.

Revealed: So let’s reveal what’s behind Line 16 of the StarlightConnector.

astro-vtbot/components/starlight/StarlightConnector.astro
function setMainTransitionScope(e: TransitionBeforePreparationEvent) {
const meta = document.querySelector('meta[name="vtbot-main-transition-scope"]');
if (!meta) return;
const mainTransitionScope = (meta as HTMLMetaElement).content || 'none';
setMainTransitionScope(document, mainTransitionScope);
setMainTransitionScope(e.newDocument, mainTransitionScope);
function setMainTransitionScope(doc: Document, value: string) {
const main = doc.querySelector(STARLIGHT_MAIN_SECTION) as HTMLElement;
main && (main.dataset.astroTransitionScope = value);
}
}

Adding more Transitions

Currently, the <main> element is the only teleport destination. If you want to add more transitions without hacking Starlight, you can add view-transition-name values to elements using pure CSS.

Example

Want to morph the old content title into the new one (on browsers with native view transition support)? Then add this to the <style> section of you new <Head> component.

"/src/components/starlight/Head.astro
1
main h1 {
2
view-transition-name: title-heading
3
}
4
::view-transition-group(title-heading) {
5
animation-duration: 0.3s;
6
}

The Pseudo-Scrolling Main Area

On browsers with native view transition support, you might see unexpected, and maybe annoying “scrolling” effects. It happens when you define view transition names for the body or large scrollable areas like the main content section of a Starlight site. It does not happen for transitions defined on the <html> root element, as this is handled differently.

I wrote about this in the overview. Jump there to see where we are in the big picture of The Bag’s Starlight support.

I need to explain the effect, its root causes and my solution in more detail. It’s best if I show you an interactive example so that you can join in.

Here you see two pages, page 1 and page 2. Both pages have a <main> element with a length that is greater than the height of the viewport.

The <main> elements have the transition:animate="slide" Astro directive set. The animation slides the old page out to the left and it slides the new one in from the right. For this demo, the durations are extended to 2 seconds so you can better observe the effects.

Press the link to the next page. Looks fine?

Now drag the scrollbar all the way down. With the next navigation, the scrollbar jumps to its top position. No smooth scrolling. An instant jump scroll. But it definitively looks like smooth scrolling. If it’s not scrolling, then what is the obvious scrolling we see?

Morphing Looks like Scrolling

What looks like scrolling is the morph animation of the two huge (in comparison to the view port) <main> elements. The browser takes the old and the new image and morphs them from the old position to the new position. The old position of the top edge of the scrolled down page is several hundred pixels negative in relation to the viewport. The position of the top edge of the new page is 0. So the morph animation moves both images down from y = -several hundred pixels to y = 0 and looking through the viewport at the new scroll position y = 0, you have the impression that the image moves down from above, or that the viewport is slowly being scrolled upwards.

This now also explains why the phenomenon can only be observed in browsers with native view transition support. Morph animations cannot be simulated.

Removing the Morph Animation

Getting rid of the morph animation is easy. Add this to your global style:

1
::view-transition-group(main) {
2
animation: none;
3
}

This removes the morph animation. When you press the link without scrolling, it looks as it did before.

Now scroll just a little bit down and watch what happens to the old image when you now press the link: When the scrollbar jumps up, the old image jumps down so that the upper edges of both pages are aligned. Then the slide animation kicks in.

Depending on your browser, you might notice that the old image might be incomplete. If the browser captures the old image of an element it should capture the entire element (plus some ink overflow). But it can clip it to reduce resources. Even then, the old image always contains the area that was visible inside the viewport before the view transition started. When the page was scrolled down completely, the capture holds at least the bottom part of the page.

Without a morph animation, both, the new and the old image, start the slide animation at the position of the new <main> element, modulo the shift added by the slide animation. The upper part of the old image is at the top of the viewport. If the browser hasn’t captured the upper part of the <main> element into the old image, you will see empty background instead. The effect could be even clearer if you really scroll all the way down before clicking on the link.

Aligning Images

We can fix the blank background by moving the old image up so that the part of the old image that overlaps with the viewport is aligned with the scroll position of the next page.

If you move the scrollbar to the middle position and then click the link, you will still see that the scrollbar jumps to the top position. But the old image does not seem to move at all. Instead of showing the possibly non-existent upper part of the old image, you will see the guaranteed current part. While the scrollbar jumped up during the navigation to the next page, we also pushed the old-image up to compensate for the scrolling.

Of course, this also should work fine if you scroll all the way to the bottom before you click the link. You can observe the same effects if you scroll the pages of the Jotter and than navigate to another page.

I have promised a reusable component for this use case. It is called <PageOffset /> and is included since astro-vtbot v1.7.10. See the component description for how to use it.