Skip to content

Sidebar

Starlight provides a sidebar, but there are some issues:

  • It doesn’t preserve state (scroll, open/closed submenus) on page switch (fixed in v0.26.0)
  • It’s not possible to have a root page for submenus

There are some workarounds:

Starlight - Scroll Current Page into View

Note: Use Starlight v0.26.0 or later instead.

  1. Create Sidebar component

    src/components/Sidebar.astro
    ---
    import type { Props } from "@astrojs/starlight/props";
    import Default from "@astrojs/starlight/components/Sidebar.astro";
    ---
    <Default {...Astro.props}><slot /></Default>
    <script>
    function isElementVisibleInContainer(element: Element, container: Element) {
    const elementRect = element.getBoundingClientRect();
    const containerRect = container.getBoundingClientRect();
    return (
    elementRect.top >= containerRect.top &&
    elementRect.left >= containerRect.left &&
    elementRect.bottom <= containerRect.bottom &&
    elementRect.right <= containerRect.right
    );
    }
    // Function to persist scroll position on page change
    function setScrollPosition() {
    const sidebar = document.querySelector("#starlight__sidebar");
    const selectedMenuItem = document.querySelector(
    "#starlight__sidebar a[aria-current='page']"
    );
    if (
    sidebar &&
    selectedMenuItem &&
    !isElementVisibleInContainer(selectedMenuItem, sidebar)
    ) {
    selectedMenuItem.scrollIntoView();
    }
    }
    setScrollPosition();
    </script>
  2. Configure Astro

    astro.config.mjs
    export default defineConfig({
    integrations: [
    starlight({
    components: {
    Sidebar: "./src/components/Sidebar.astro",
    },
    }),
    ],
    });

Alternative sidebars for Starlight

Starlight provides configurable sidebar, which is good enough for documentation. For digital garden we may want to use different approach, for example:

Alphabetical list

src/components/Sidebar.astro
---
import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/Sidebar.astro";
import { getBrainDb } from "@braindb/astro";
import { isContent } from "@lib/braindb.mjs";
import type { Document } from "@braindb/core";
const firstChar = (str: string) => String.fromCodePoint(str.codePointAt(0)!);
const docsByChar = new Map<string, Document[]>();
(await getBrainDb().documents()).forEach((doc) => {
if (isContent(doc)) {
const char = firstChar(doc.title()).toUpperCase();
docsByChar.set(char, docsByChar.get(char) || []);
docsByChar.get(char)?.push(doc);
}
});
const comparator = new Intl.Collator("en");
const sidebarAlpha = [...docsByChar.keys()]
.sort(comparator.compare)
.map((char) => {
let collapsed = true;
return {
type: "group",
label: char,
entries: docsByChar.get(char)?.map((doc) => {
const isCurrent = doc.path() === `/${Astro.props.id}`;
if (isCurrent) collapsed = false;
return {
type: "link",
href: doc.url(),
isCurrent,
// @ts-ignore
label: doc.frontmatter()?.["sidebar"]?.["label"] || doc.title(),
// @ts-ignore
badge: doc.frontmatter()?.["sidebar"]?.["badge"],
// @ts-ignore
attrs: doc.frontmatter()?.["sidebar"]?.["attrs"] || {},
};
}),
collapsed,
badge: undefined,
};
}) as Props["sidebar"];
sidebarAlpha.unshift(Astro.props.sidebar[0]);
---
<Default {...Astro.props} sidebar={sidebarAlpha}><slot /></Default>

Articles grouped by date

src/components/Sidebar.astro
---
import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/Sidebar.astro";
import { getBrainDb } from "@braindb/astro";
import { isContent } from "@lib/braindb.mjs";
import type { Document } from "@braindb/core";
const docsByDate = new Map<string, Document[]>();
(await getBrainDb().documents({ sort: ["updated_at", "desc"] })).forEach((doc) => {
if (isContent(doc)) {
const date = doc.updatedAt().toISOString().split("T")[0];
docsByDate.set(date, docsByDate.get(date) || []);
docsByDate.get(date)?.push(doc);
}
});
const sidebarDates = Array.from(docsByDate.keys())
.map((d) => [d, d.split("-").reverse().join(".")])
.map(([date, label]) => {
let collapsed = true;
return {
type: "group",
label,
entries: docsByDate.get(date)?.map((doc) => {
const isCurrent = doc.path() === `/${Astro.props.id}`;
if (isCurrent) collapsed = false;
return {
type: "link",
href: doc.url(),
isCurrent,
// @ts-ignore
label: doc.frontmatter()?.["sidebar"]?.["label"] || doc.title(),
// @ts-ignore
badge: doc.frontmatter()?.["sidebar"]?.["badge"],
// @ts-ignore
attrs: doc.frontmatter()?.["sidebar"]?.["attrs"] || {},
};
}),
collapsed,
badge: undefined,
};
}) as Props["sidebar"];
sidebarDates.unshift(Astro.props.sidebar[0]);
---
<Default {...Astro.props} sidebar={sidebarDates}><slot /></Default>

Articles grouped by tags

src/components/Sidebar.astro
---
import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/Sidebar.astro";
import { getBrainDb } from "@braindb/astro";
import { isContent } from "@lib/braindb.mjs";
import type { Document } from "@braindb/core";
const docsByTags = new Map<string, Document[]>();
(await getBrainDb().documents()).forEach((doc) => {
if (isContent(doc)) {
// @ts-expect-error
doc.frontmatter().tags.forEach((tag: string) => {
docsByTags.set(tag, docsByTags.get(tag) || []);
docsByTags.get(tag)?.push(doc);
});
}
});
const comparator = new Intl.Collator("en");
const sidebarTags = [...docsByTags.keys()]
.sort(comparator.compare)
.map((tag) => {
let collapsed = true;
return {
type: "group",
label: `#${tag}`,
entries: docsByTags.get(tag)?.map((doc) => {
const isCurrent = doc.path() === `/${Astro.props.id}`;
if (isCurrent) collapsed = false;
return {
type: "link",
href: doc.url(),
isCurrent,
// @ts-ignore
label: doc.frontmatter()?.["sidebar"]?.["label"] || doc.title(),
// @ts-ignore
badge: doc.frontmatter()?.["sidebar"]?.["badge"],
// @ts-ignore
attrs: doc.frontmatter()?.["sidebar"]?.["attrs"] || {},
};
}),
collapsed,
badge: undefined,
};
}) as Props["sidebar"];
sidebarTags.unshift(Astro.props.sidebar[0]);
---
<Default {...Astro.props} sidebar={sidebarTags}><slot /></Default>

Multiple sidebars experiment

src/components/Sidebar.astro
---
import { Tabs, TabItem } from "@astrojs/starlight/components";
// ... other code see above
---
<Tabs>
<TabItem label="Default">
<Default {...Astro.props}><slot /></Default>
</TabItem>
<TabItem label="A-Z">
<Default {...Astro.props} sidebar={sidebarAlpha}><slot /></Default>
</TabItem>
<TabItem label="Latest">
<Default {...Astro.props} sidebar={sidebarDates}><slot /></Default>
</TabItem>
</Tabs>

TODO

  • Render as partials and fetch from the server instead of prerendering, because there are too many DOM nodes.
  • Tabs should preserve state across navigations.
  • The scroll area should be inside the TabItem.