◆◆◆◆◆claude-fable-51 person reached

part of BrainFables

The scrollbar that nudged the whole site sideways

Why navigating to a long page shifted the entire layout left, and how two CSS properties — scrollbar-gutter and scrollbar-color — fixed the jump and themed every scrollbar at once.

0
by Joe (0 rep)cssscrollbarlayout-shifttheming

Click from a short page to /format on brainfables.com and the whole layout lurched a few pixels to the left. Click back, and it lurched right again. The culprit: the /format page is long, so the browser shows a vertical scrollbar — and a classic (non-overlay) scrollbar takes its width out of the viewport, squeezing the centered content over. Short pages get the full width; long pages don't. Every navigation between the two is a visible jump.

The request was twofold: a scrollbar should never shift the UI on any screen, and while we're at it, the scrollbar should match the app's paper-and-ink theme instead of the stock OS gray.

The investigation

The recon was short. src/styles.css is the app's single global stylesheet, and a grep showed it contained no scrollbar rules at all — the document scroller (the <html>/<body> element) was entirely at the browser's mercy. One nuance surfaced, though: the generated src/styles/expressive-code.css does style scrollbars, scoped to code frames:

src/styles/expressive-code.css (generated, excerpt)
.expressive-code pre::-webkit-scrollbar-thumb {
background-color: var(--ec-sbThumbCol);
border: 4px solid transparent;
background-clip: content-box;
border-radius: 10px;
}

So any global theming had to coexist with that, not fight it.

The fix: two properties, one guard

Everything landed in src/styles.css, right after the ::selection rule:

src/styles.css
html {
scrollbar-gutter: stable;
scrollbar-color: var(--ink-faint) transparent;
}

scrollbar-gutter: stable tells the browser to reserve the scrollbar's width on the root scroller permanently, whether or not the page overflows. Short pages now show an empty gutter where the scrollbar would be; long pages fill it in. Either way the content column never moves. It also covers the inverse jump — a modal setting overflow: hidden on the body keeps the gutter reserved instead of letting the page widen.

scrollbar-color: <thumb> <track> is the modern, standard way to theme a scrollbar — here a warm taupe thumb (--ink-faint, the design system's faint-ink token) on a transparent track. Crucially, scrollbar-color is an inherited property, so setting it once on html themes every scroller on the site: the document, the format-prompt <pre>, comment panels, the editor textarea.

supports it@supports not(scrollbar-color:auto)Which browser?Chrome 121+ /FirefoxSafariscrollbar-color(inherited)::-webkit-scrollbar fallback

Safari doesn't support scrollbar-color, so it gets the old WebKit pseudo-element styling — but guarded:

src/styles.css
@supports not (scrollbar-color: auto) {
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--ink-faint);
border: 3px solid transparent;
background-clip: content-box;
border-radius: 6px;
}
}
Why the @supports guard matters◆◆◆◆◆go deeper +

Chrome supports both mechanisms, with a rule for resolving the conflict: once an element has a non-auto scrollbar-color, Chrome ignores its ::-webkit-scrollbar styling entirely. Without the guard, Chrome's behavior would depend on which mechanism wins per element — and since scrollbar-color inherits everywhere, the unguarded WebKit rules would be dead weight at best and a confusing half-applied theme at worst.

The guard makes the split clean: browsers that understand scrollbar-color (Chrome 121+, Firefox forever, Safari 18.2+) use only the standard property; older Safari falls into the @supports not branch and uses only the pseudo-elements. The border: 3px solid transparent + background-clip: content-box trick insets the thumb from the track edges, mimicking the padding native scrollbars have.

One accepted trade-off: the inherited scrollbar-color also overrides expressive-code's own gray thumb inside code frames (same Chrome rule — standard property beats the pseudo-elements). That's fine: a warm ink thumb on the code block's white background is more consistent with the site, not less.

Verification

CSS has no unit-test surface, so the gates were: npx tsc --noEmit clean, all 16 vitest tests passing, a production build succeeding, and a grep proving the rules survived the Tailwind v4 pipeline into the built bundle:

integration check
grep -o "scrollbar-gutter:stable[^;]*;scrollbar-color:[^}]*" dist/client/assets/*.css
# dist/client/assets/styles-Bs7Hxo2i.css:scrollbar-gutter:stable;scrollbar-color:var(--ink-faint) transparent

Check yourself

Why does setting scrollbar-color only on the html element theme every scrollable box on the page?

Generated with claude-fable-5 · curated by Joe · CC BY-SA 4.0 · v1

Discussion 0 comments — open a section's margin marker, or select its text, to comment in place

Nothing here yet.

0/2000