Shadowing Starlight Components for Layout Tweaks
Why we shadow components
Section titled “Why we shadow components”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.
Files we shadow
Section titled “Files we shadow”| Component | Purpose | Local path |
|---|---|---|
TwoColumnContent.astro | Defines the main/toc flex layout and widths. | src/components/starlight/TwoColumnContent.astro |
PageSidebar.astro | Styles 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.
Current implementation
Section titled “Current implementation”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.
When Starlight updates
Section titled “When Starlight updates”-
Check upstream changes.
Compare our shadowed file to the version innode_modules/@astrojs/starlight/components/. If the upstream layout changed, merge those differences first, then reapply our customisations. -
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. -
Restart the dev server.
Shadowed components are cached; restartnpm run devso Astro picks up the edits.
Editing checklist for future contributors
Section titled “Editing checklist for future contributors”- Need to change right-column width? Update
--sl-right-sidebar-widthinTwoColumnContent.astro. - Want different padding or link wrapping? Tweak the
.right-sidebar-panelrules inPageSidebar.astro. - Prefer to adjust typography/colors? Do that in
src/styles/global.css; it still loads everywhere. - Always rerun
npm run devafter modifying components insidesrc/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.