fix: pin "back to top" to opacity 0 in the critical CSS - #168
Conversation
BackToTop is hidden by opacity-0 and carries transition-opacity, so on any frame where the stylesheet is absent it paints at full opacity and fades out once the sheet lands. That frame is produced by the React #423 hydration recovery (#126), which re-renders the head and briefly drops the stylesheet after the component has mounted — so gating the transition on mount, as #141 tried, cannot prevent it. Measured in WebKit with the stylesheet delayed: 17-18 animating frames without this rule, none with it. The FOUC guard asserts the rule: the main test expects opacity 0, the control expects 1 with the inline block stripped. Supersedes the Outline.tsx change in #141. Relates to #126. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
🎭 Visual regression resultsDetails
Skipped testsmobile-chrome › theme.spec.ts › QuantEcon theme — visual regression › without JavaScript › drawer-opens-without-javascript |
There was a problem hiding this comment.
🟢 Approval recommended
The change is a small, zero-specificity additive critical-CSS rule verified not to affect live state, backed by consistent, null-guarded FOUC guard assertions.
Pull request overview
This PR fixes the "back to top" button fading out of the page margin during static-build page loads. Rather than gating the transition on mount (the superseded #141 useMounted approach, which cannot cover the React #423 hydration-recovery frame that re-renders the head and drops the stylesheet after mount), it pins the button to opacity: 0 via a zero-specificity :where() rule in the inlined critical CSS — the same mechanism already used for the toggle's close icon. The WebKit FOUC guard is extended to assert the new behavior.
Changes:
- Add
:where(.qe-back-to-top){opacity:0}toCRITICAL_CSSinapp/root.tsx, with explanatory comments. - Extend
tests/visual/fouc.spec.tswith abackToTopOpacityprobe: main test expects"0", control expects"1". - Document the fix in
CHANGELOG.mdunder[Unreleased].
File summaries
| File | Description |
|---|---|
app/root.tsx |
Adds the zero-specificity :where(.qe-back-to-top){opacity:0} critical-CSS rule and comments explaining the hydration-recovery rationale. |
tests/visual/fouc.spec.ts |
Adds a backToTopOpacity probe and null-guarded assertions in both the main and control cases. |
CHANGELOG.md |
Records the fix under [Unreleased]/Fixed, noting it supersedes the #141 approach. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…cal-css # Conflicts: # CHANGELOG.md
Pins the "back to top" button to
opacity: 0in the critical CSS, on the same zero-specificity terms as the toggle's close icon from #144, and has the WebKit FOUC guard assert it.Why a critical-CSS rule and not the
useMountedgate from #141BackToTopis hidden byopacity-0and carriestransition-opacity, so on any frame where the stylesheet is absent it paints at full opacity and then fades out when the sheet lands. #141 assumed that frame is the initial paint and withheld the transition until after mount. Measured in WebKit against amake build-themebundle ofmain, that is not where the frame comes from: WebKit blocks first paint on the head stylesheets here, and even withapp.cssdelayed by 400ms the first painted frame was already styled. The unstyled frame arrives about 200ms afterDOMContentLoaded, when the React#423hydration recovery (#126) re-renders the head and briefly drops the stylesheet. By then the component is mounted and its transition is live, so gating on mount changes nothing.Per-frame opacity of
.qe-back-to-topwith the stylesheet delayed 400ms, three runs each:mainmain+ #141'sOutline.tsxchangemain+ this ruleThe inline
<style>is restored with the tree on that re-render, which is why a rule in it holds through the gap. Itsopacity-100utility outranks the:where()rule, so the button still shows once scrolled (checked: 0 at the top of the page, 1 after scrolling 600px).Test
fouc.spec.tsgains abackToTopOpacityprobe next to the close-icon one. The main test expects"0"(pinned by the inline rule); the control expects"1"(with the rule stripped the button paints, which is the state the transition would animate away from). Both guard against the element going missing first, so a rename fails as itself.Verified the guard still guards: removing the rule from
CRITICAL_CSSfails the main test onbackToTopOpacity(Expected: "0" / Received: "1") while the control still passes.npm run compileclean ·test:unitall passing ·test:fouc2 passed.Relates to #126 (root cause, stays open), #127 (closed by #144) and #141 (superseded).
🤖 Generated with Claude Code