You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The website's docs / ui sidebar drawer locks page scroll with a CSS-only rule, and that lock shifts the fixed site header. Same bug class as #1144, different code path, so the fix that landed for <ui-dialog> does not reach it.
website/lib/docs-shell.ts:252, inside the @media (max-width: 859.98px) block opened at L210:
body[data-docs-nav-open] { overflow: hidden; }
Hiding the page scrollbar widens the viewport by its width. The site header is position: fixed (website/app/layout.ts:348, class="site-top fixed inset-x-0 top-0 z-20"), so it lays out against the initial containing block and widens with the viewport.
Measured in Chromium with a classic 15px scrollbar, at a 800x700 viewport, on /docs/styling:
header width
right-hand control
--wj-scrollbar-compensation
drawer closed
785
right edge at 761
unset
drawer open
800
right edge at 776
unset
So the theme toggle and menu button jump right by the full 15px. The left-aligned brand does not move, which is why this reads as a subtler jump than #1144 (where a centred nav moved by half the width).
Only reproduces where the scrollbar takes layout width: classic scrollbars on Windows and Linux, and macOS set to "Show scroll bars: Always". It needs a viewport under 860px, which on those platforms means a narrowed desktop window, since a phone has overlay scrollbars. That narrow band is why it has gone unnoticed, not a reason it is not real.
Design / approach
The #1144 fix cannot be reused directly. That one is a JS lock, so it measures the residual and publishes --wj-scrollbar-compensation for a fixed element to opt into. This lock is pure CSS with no JS involved, so there is nothing to measure and the property is never set. The .site-top opt-in added in #1144 is therefore inert here.
Reserve the gutter declaratively instead, keyed off the same attribute. The gutter has to be on the root element, not on body: scrollbar-gutter on body does NOT propagate to the viewport (verified while working #1144, where the body-scoped variant left the header shifting by the full amount while the html-scoped one held it exactly).
WebKit ignores scrollbar-gutter entirely (measured in fix(ui): opening a dialog shifts a fixed header right by half the scrollbar #1144: applying stable to a short page narrows the ICB on Chromium and does nothing on WebKit). So this fixes Chromium and Firefox and leaves WebKit as is. Decide whether that is acceptable or whether the drawer toggle should set the property from the JS that already toggles the attribute (website/app/layout.ts:203), which would let the existing .site-top opt-in cover WebKit too.
Reserving the gutter only while the drawer is open means the width changes at the moment the rule applies, which is exactly what it is meant to prevent. Confirm empirically that the gutter is reserved in the same layout pass that removes the scrollbar and there is no one-frame flash.
An alternative worth weighing: move the lock into the JS that already owns the attribute (closeDocsNav / the toggle handler in website/app/layout.ts), and reuse the #1144 lock shape verbatim. That costs the CSS-only simplicity but gets measurement, the WebKit fallback, and one lock implementation instead of two.
Implementation notes (for the implementing agent)
Where to edit:
website/lib/docs-shell.ts:252, the body[data-docs-nav-open] { overflow: hidden; } rule inside the @media (max-width: 859.98px) block that opens at L210.
website/app/layout.ts if taking the JS route: the toggle at L203 (document.body.toggleAttribute('data-docs-nav-open')) and closeDocsNav() at L173. Note the root layout deliberately owns these handlers so the shell module stays inert at load and its importers can be elided. Do not move them into docs-shell.ts.
Headless browsers hide scrollbars, so this does not reproduce by default. Playwright passes --hide-scrollbars; drop it with ignoreDefaultArgs: ['--hide-scrollbars']. The root WTR config already does this for Chromium (see fix(ui): opening a dialog shifts a fixed header right by half the scrollbar #1144). A test that passes without a measurable scrollbar proves nothing.
Measure a RIGHT-aligned or centred element. The brand is left-aligned and does not move, so measuring it reports a false pass.
document.documentElement.clientWidth is not a reliable probe here: under a reserved gutter Chromium reports it growing while the fixed header does not move. Measure element rects.
The drawer state survives soft navigation by design (the attribute is on <body>, outside every swap range). Any state added alongside it must be cleared on the same paths, or it leaks across navigations.
Invariants to respect:
Repo root AGENTS.md invariant 11 for any prose added.
The shell module must stay inert at load (see the comment block at website/app/layout.ts:151), so no module-scope client work in docs-shell.ts.
Tests + docs surfaces:
website/web-test-runner.config.js has its own browser test config; check whether a drawer test belongs there or in the root suite.
Problem
The website's docs / ui sidebar drawer locks page scroll with a CSS-only rule, and that lock shifts the fixed site header. Same bug class as #1144, different code path, so the fix that landed for
<ui-dialog>does not reach it.website/lib/docs-shell.ts:252, inside the@media (max-width: 859.98px)block opened at L210:Hiding the page scrollbar widens the viewport by its width. The site header is
position: fixed(website/app/layout.ts:348,class="site-top fixed inset-x-0 top-0 z-20"), so it lays out against the initial containing block and widens with the viewport.Measured in Chromium with a classic 15px scrollbar, at a 800x700 viewport, on
/docs/styling:--wj-scrollbar-compensationSo the theme toggle and menu button jump right by the full 15px. The left-aligned brand does not move, which is why this reads as a subtler jump than #1144 (where a centred nav moved by half the width).
Only reproduces where the scrollbar takes layout width: classic scrollbars on Windows and Linux, and macOS set to "Show scroll bars: Always". It needs a viewport under 860px, which on those platforms means a narrowed desktop window, since a phone has overlay scrollbars. That narrow band is why it has gone unnoticed, not a reason it is not real.
Design / approach
The #1144 fix cannot be reused directly. That one is a JS lock, so it measures the residual and publishes
--wj-scrollbar-compensationfor a fixed element to opt into. This lock is pure CSS with no JS involved, so there is nothing to measure and the property is never set. The.site-topopt-in added in #1144 is therefore inert here.Reserve the gutter declaratively instead, keyed off the same attribute. The gutter has to be on the root element, not on body:
scrollbar-gutteron body does NOT propagate to the viewport (verified while working #1144, where the body-scoped variant left the header shifting by the full amount while the html-scoped one held it exactly).Two things to confirm before committing to it:
scrollbar-gutterentirely (measured in fix(ui): opening a dialog shifts a fixed header right by half the scrollbar #1144: applyingstableto a short page narrows the ICB on Chromium and does nothing on WebKit). So this fixes Chromium and Firefox and leaves WebKit as is. Decide whether that is acceptable or whether the drawer toggle should set the property from the JS that already toggles the attribute (website/app/layout.ts:203), which would let the existing.site-topopt-in cover WebKit too.An alternative worth weighing: move the lock into the JS that already owns the attribute (
closeDocsNav/ the toggle handler inwebsite/app/layout.ts), and reuse the #1144 lock shape verbatim. That costs the CSS-only simplicity but gets measurement, the WebKit fallback, and one lock implementation instead of two.Implementation notes (for the implementing agent)
Where to edit:
website/lib/docs-shell.ts:252, thebody[data-docs-nav-open] { overflow: hidden; }rule inside the@media (max-width: 859.98px)block that opens at L210.website/app/layout.tsif taking the JS route: the toggle at L203 (document.body.toggleAttribute('data-docs-nav-open')) andcloseDocsNav()at L173. Note the root layout deliberately owns these handlers so the shell module stays inert at load and its importers can be elided. Do not move them intodocs-shell.ts.website/app/layout.tsalready carries the.site-top { padding-right: var(--wj-scrollbar-compensation, 0px); }opt-in from fix(ui): opening a dialog shifts a fixed header right by half the scrollbar #1144. If the JS route is taken, that line needs no change and starts working for the drawer too.Landmines / gotchas:
--hide-scrollbars; drop it withignoreDefaultArgs: ['--hide-scrollbars']. The root WTR config already does this for Chromium (see fix(ui): opening a dialog shifts a fixed header right by half the scrollbar #1144). A test that passes without a measurable scrollbar proves nothing.document.documentElement.clientWidthis not a reliable probe here: under a reserved gutter Chromium reports it growing while the fixed header does not move. Measure element rects.<body>, outside every swap range). Any state added alongside it must be cleared on the same paths, or it leaks across navigations.Invariants to respect:
website/app/layout.ts:151), so no module-scope client work indocs-shell.ts.Tests + docs surfaces:
website/web-test-runner.config.jshas its own browser test config; check whether a drawer test belongs there or in the root suite..agents/skills/webjs/references/styling.mdalready documents the fixed-header-plus-scroll-lock contract (added in fix(ui): opening a dialog shifts a fixed header right by half the scrollbar #1144). If the drawer takes a different shape than the kit's lock, note it there so the two do not silently diverge.Acceptance criteria