fix: stop the contents sidebar flashing open on page load - #123
Conversation
On the static build every navigation is a full document load, and the first paint can happen before app.css applies. Until it does, `-translate-x-full` means nothing, so the nav panel painted in-flow and fully visible. When the stylesheet finally arrived the transform resolved, and because `transition-all` was already on the element the correction was animated — the menu appeared to open, then slide shut over 300ms. Two guards, since either alone leaves a visible artefact: - Park the panel off-screen in the inlined critical CSS, so it is never visible on the first paint. Width/height/position match the Tailwind classes so the resolved transform is identical before and after app.css lands and nothing animates on arrival. - Withhold the transition classes until after mount, so any remaining correction is applied instantly rather than animated. Also narrows `transition-all` to `transition-transform`: the former animated 15 properties (background, borders, padding, tab-size) where only the slide was wanted. Covered by the existing FOUC guard, which already isolates the inline critical CSS and now asserts the panel starts off-screen.
|
🎭 Visual regression resultsDetails
Skipped testsmobile-chrome › theme.spec.ts › QuantEcon theme — visual regression › launch-colab |
|
Nice piece of debugging — the diagnosis is right and the primary guard demonstrably works. I built each variant and traced the panel frame-by-frame in WebKit (serve
Also green: One thing I'd change before merge — the new sync-list comment in
Neither half quite holds. Resolved widths are 350 / 250 / 350 across the three bands (base A few smaller notes, take or leave:
Two things I hit while reviewing are pre-existing and out of scope here, so I filed them separately: #126 (React hydration failing on every load, which also makes the FOUC control test racy) and #127 (the sidebar toggle icons animate the same first-paint correction). |
|
@DrDrij here is a review pass. Can you let me know if this is helpful or not? Thanks. |
The contents sidebar flashes open for a moment on page load, then slides shut. Reported against the Netlify preview of QuantEcon/lecture-python-programming#363.
Cause
Not a state bug — the server-rendered markup is correct and already carries
-translate-x-full.On a static build every navigation is a full document load, and the first paint can happen before
app.cssapplies. This is the same FOUC as quantecon-theme-src#66, which the inlined critical CSS already addresses for the font and the content grid — but that critical CSS never covered the sidebar.So the sequence is:
app.css.-translate-x-full,fixedandw-[250px]all mean nothing, so the panel lays out as a plain in-flow block, full width and fully visible.app.cssarrives andtransformresolves fromnonetotranslateX(-100%).transition-all duration-300had been on the element since that first paint, the browser animates the correction.The menu is never actually opened. It was simply never hidden, and then took 300ms to put itself away — which is why a one-frame glitch reads as a deliberate animation.
Measured
Two builds of this repo, served with
myst starton the visual fixture.app.cssis served empty so the first paint happens unstyled — the field condition — then the real stylesheet is applied and the panel traced every frame.Note that simply delaying
app.cssdoes not reproduce this: Chrome then blocks rendering and never paints, which is the healthy path. The flash needs a paint that beats the stylesheet.Fix
Two guards, because either alone still leaves an artefact:
app/root.tsx— a.qe-contents-sidebarrule in the existing critical CSS parks the panel off-screen on the first paint. Its width/position match the Tailwind classes so the resolved transform is identical before and afterapp.csslands, meaning nothing can animate on arrival.app/components/ContentsSidebar.tsx— the transition classes are withheld until after mount, so any residual correction is applied instantly rather than animated.transition-allis also narrowed totransition-transform. The former animated 15 properties when only the slide was ever wanted; the latter is compositor-only.Opening and closing the menu is unaffected — it still animates normally once mounted (
-250 -> 0 -> -250, animatingtransformalone).A trap worth recording
The critical CSS uses
:where(), so it carries zero specificity. That means every property set there must also be declared by the real stylesheet, or it can never be overridden. Hiding the panel withvisibility: hiddenwould have pinned it shut permanently, since no Tailwind class setsvisibility. Hence the transform. This constraint is now documented in the block comment.Tests
The existing WebKit FOUC guard covers this, since the cause and the guard are the same. It now also asserts the panel starts off-screen — and its control case asserts the flash does reproduce when the inline rule is removed, so the guard cannot silently rot.