Skip to content

Shadowing Starlight Components for Layout Tweaks

We tried overriding Starlight’s layout purely through src/styles/global.css, but the site flashed between the default layout and our custom layout on every page load. The browser executed Starlight’s built-in CSS first and only applied our overrides once the external sheet finished downloading. Result: a visible jump, bad UX, and confused contributors.

Shadowing copies Starlight’s core components into src/components/starlight/. Astro loads our versions instead of the originals, so the generated CSS bundle already includes our layout rules. The page renders in the final state immediately—no duplicate styles, no inline hacks, and one obvious place to edit the layout.

ComponentPurposeLocal path
TwoColumnContent.astroDefines the main/toc flex layout and widths.src/components/starlight/TwoColumnContent.astro
PageSidebar.astroStyles the table-of-contents panel.src/components/starlight/PageSidebar.astro

Those are the only layout pieces we override. Everything else (theme colors, typography, Tailwind utilities) stays in src/styles/global.css.

Key details inside src/components/starlight/TwoColumnContent.astro:

@layer starlight.core {
:global(:root) {
--sl-sidebar-width: 20rem;
--sl-content-width: 58rem;
--sl-right-sidebar-width: 12rem;
--sl-right-sidebar-pad-x: 0.5rem;
}
@media (min-width: 72rem) {
.right-sidebar-container {
order: 2;
flex: 0 0 var(--sl-right-sidebar-width);
width: var(--sl-right-sidebar-width);
}
.right-sidebar {
position: sticky;
top: var(--sl-nav-height);
height: calc(100vh - var(--sl-nav-height));
overflow-y: auto;
}
:global([data-has-sidebar][data-has-toc]) .main-pane {
--sl-content-margin-inline: 0 auto;
flex: 1 1 auto;
max-width: var(--sl-content-width);
margin-inline: 0 auto;
}
}
}

The companion tweaks in src/components/starlight/PageSidebar.astro make the table-of-contents container respect the new variable and wrap long headings:

@layer starlight.core {
.right-sidebar-panel {
--_pad-x: var(--sl-right-sidebar-pad-x, var(--sl-sidebar-pad-x));
padding: 1rem var(--_pad-x);
}
.right-sidebar-panel .sl-container {
--_pad-x: var(--sl-right-sidebar-pad-x, var(--sl-sidebar-pad-x));
width: calc(var(--sl-right-sidebar-width) - 2 * var(--_pad-x));
max-width: calc(var(--sl-right-sidebar-width) - 2 * var(--_pad-x));
}
.right-sidebar-panel :global(:where(a)) {
overflow-wrap: anywhere;
white-space: normal;
}
}

Adjust --sl-right-sidebar-width and --sl-right-sidebar-pad-x to tune the TOC width. Everything lives at the top of TwoColumnContent.astro, so future edits happen in one place.

  1. Check upstream changes.
    Compare our shadowed file to the version in node_modules/@astrojs/starlight/components/. If the upstream layout changed, merge those differences first, then reapply our customisations.

  2. Test across breakpoints.
    Run through desktop (≥72rem), medium, and mobile widths to ensure the sticky right sidebar, media queries, and root variables still behave correctly.

  3. Restart the dev server.
    Shadowed components are cached; restart npm run dev so Astro picks up the edits.

  • Need to change right-column width? Update --sl-right-sidebar-width in TwoColumnContent.astro.
  • Want different padding or link wrapping? Tweak the .right-sidebar-panel rules in PageSidebar.astro.
  • Prefer to adjust typography/colors? Do that in src/styles/global.css; it still loads everywhere.
  • Always rerun npm run dev after modifying components inside src/components/starlight/.

By keeping the structural CSS inside the shadowed components, we avoid flicker, lower specificity battles, and give future agents a single obvious place to work. Share additional layout patterns in this folder so nobody has to rediscover the approach.