Skip to content

Last updated: First published:

Updating the Sidebar

The story to be told about the sidebar is a bit convoluted.

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

The Bag’s view transition support keeps the app frame of the Starlight site unchanged and only swaps the main-frame inside it on transitions. While you could use the sidebar for navigation this way, it would look a bit odd if the current page marker wouldn’t change. So we need some script to update the current page marker in the sidebar ourselves. And that’s it.

At least that’s what I thought.

What it Could have Been

And that would have been a a nice exercise:

  • Remove the aria-current="page" attribute where ever it exists in e.newDocument. This is the marker we would update.
  • Also in e.newDocument, find the sidebar entry that links to e.to.href. Add the aria-current="page" to that element. Done. Run all that directly after the loader has fetched the new DOM.

Splash! Sidebar Changes on Navigation

To be honest, I did the exercise above for v1.7.0 of The Bag and thought it was good as it was. But then I learned it wasn’t. I had another dependency on Starlight’s page structure that I wasn’t aware of before: If you use the splash template, the sidebar is not created at all.

You may have notice that the Jotter is linked from The Bag’s homepage. It doesn’t use a splash screen as entry. But almost all others Starlight sites do.

Starting with a splash screen creates an app frame without a sidebar. If you jump to a page with a doc template, we keep this app frame and never show a sidebar (until we force the browser to do a full page load). Equally irritating, if you navigate from a doc page to a splash page, we retain the app frame, which bravely continues to display the sidebar on the splash screen.

So this is the point at which the first assumption proved to be wrong.

Swapping the Sidebar

We can’t just add data-vtbot-replace to the sidebar and trust the <ReplacementSwap/> component to do it right. It would work to throw away the sidebar when navigating to a splash page, but it might not add a missing sidebar. But the special handling we need for the sidebar is very similar to what ReplacementSwap does internally.

Revealed: This now reveals what is behind Line 20 of the StarlightConnector.

astro-vtbot/components/starlight/StarlightConnector.astro
function updateSidebar(e: TransitionBeforeSwapEvent) {
const newSidebar = e.newDocument.querySelector(STARLIGHT_SIDEBAR);
if (!newSidebar) {
document.querySelector(STARLIGHT_SIDEBAR)?.remove();
} else {
const sidebar = document.querySelector(STARLIGHT_SIDEBAR);
if (!sidebar) {
document
.querySelector(STARLIGHT_MAIN_FRAME)
?.insertAdjacentElement('beforebegin', newSidebar);
} else {
const oldContent = sidebar.querySelector(STARLIGHT_SIDEBAR_CONTENT);
const newContent = newSidebar.querySelector(STARLIGHT_SIDEBAR_CONTENT);
if (oldContent && newContent) {
oldContent.replaceWith(newContent);
} else {
sidebar.replaceWith(newSidebar);
}
}
}
}
  • If we do have a sidebar, but the new page shouldn’t, we drop it.
  • If we don’t have a sidebar but the new page should have one, we add it.
  • If both pages have a sidebar, we copy the new content to the current sidebar.

Note that we try to avoid copying the sidebar itself to keep the current scroll position for longer sidebars.

If a Starlight site decides to render page-specific content in the sidebar, that would also work quite well.

And the Current Page Marker?

That does not need special attention any more. The aria-current="page" is automatically updated when copying the sidebar content. This would even work if the collapse

The Twist in the Tale

When I showed this to @Chris, the mastermind behind Starlight, he mentioned that people would like it if the sidebar remembered the state of the collapsed categories during navigation.

Like the scroll position, this information is a inherent part of the state of the sidebar. If you don’t change it during navigation, it should still be the same on the next page.

I was a little confused.

  1. I hadn’t actively thought about this kind of state before.
  2. I had the desired behavior in my first version where i didn’t handle the sidebar specifically, but just updated the highlighting of the current page. This feature went completely unnoticed by me.
  3. I just ruined it by overriding this valuable information about the categories by replacing the sidebar content.

I’ve been thinking about this: people who want the status of categories saved are much more common than authors who want to change the sidebar content. I could just delete the last 6 lines of updateSidebar() and be done.

On the other hand: Starlight will provide a built-in solution to maintain the state of the sidebar during navigation, regardless of view transitions. For this to work, the current version of updateSidebar() could be much more robust, but who knows. And then there would be the use case of authors changing the sidebar content depending on the current page …

If you don’t know what to do, make it someone else’s problem. I add a switch that defaults to the “only update the current page marker” version and in this case, skips replacing existing sidebar content in updateSidebar(). Authors can set it to what fits best for their site.

Luckily, I still had that code I sketched at the beginning of this page.

astro-vtbot/components/starlight/StarlightConnector.astro
function updateCurrentPageMarker(e: TransitionBeforePreparationEvent) {
document
.querySelectorAll('[aria-current="page"]')
.forEach((el) => el.removeAttribute('aria-current'));
const currentPage = document.querySelector(
`${STARLIGHT_SIDEBAR_CONTENT} a[href="${e.to.pathname}"]`
);
currentPage?.setAttribute('aria-current', 'page');
}

Revealed: This listing reveals what’s behind Line 14 of the StarlightConnector.

It now again runs by default. You can switch it off in favor of the “replace existing sidebar content on navigation” semantics. Maybe this might be necessary when Starlight comes with its own sidebar state handling. Here is how:

./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} replaceSidebarContent>
<StarlightHead {...Astro.props}><slot /></StarlightHead>
</VtbotStarlight>

And the Mobile Menu Button?

Revealed: How do we close the mobile menu on navigation? This reveals what is hidden behind Line 15 of the StarlightConnector.

astro-vtbot/components/starlight/StarlightConnector.astro
function closeMobileMenu() {
if (document.body.hasAttribute(STARLIGHT_MOBILE_MENU_EXPANDED)) {
document.body
.querySelector(STARLIGHT_MENU_BUTTON)
?.closest('nav')
?.dispatchEvent(
new KeyboardEvent('keyup', {
key: 'Escape',
code: 'Escape',
charCode: 27,
keyCode: 27,
shiftKey: false,
ctrlKey: false,
altKey: false,
metaKey: false,
})
);
}
}

It just presses the escape key if it detects that the mobile menu is open.