Fix layout shift on home page load (font swap + menu band hydration)#910
Open
RisingOrange wants to merge 5 commits into
Open
Fix layout shift on home page load (font swap + menu band hydration)#910RisingOrange wants to merge 5 commits into
RisingOrange wants to merge 5 commits into
Conversation
Two stacked causes: 1. Webfonts use font-display: swap, so the page first paints with fallback fonts (Impact / serif) whose metrics differ a lot from Saira Condensed and Roboto Slab. When the webfonts arrive, the nav and hero text reflow and everything below jumps (~36px at 1280px). Fixed with fontaine, which generates '<font> fallback' @font-face rules with size-adjusted metrics. The fallback lists include Tinos/Arimo/Noto so src:local() also resolves on Linux. 2. The pre-hydration --menu-orange defaults (128px / 280px) didn't match the band height JS measures after hydration (142px / 180px / 213px depending on menu wrapping), so the orange band behind the menu jumped again once hydration ran. Defaults now match the measured values per breakpoint. Verified with a Playwright probe (fonts delayed 2s): hero, slogan and campaign rects are now identical before and after the font swap at 1280px, 700px and 375px viewports; previously the hero shrank 36.2px and then grew 14px.
👷 Deploy request for pauseai pending review.Visit the deploys page to approve it
|
The previous commit hardcoded the band's pre-hydration heights (142/180/213px), but those were measured for English nav labels — other locales wrap the menu differently, so the hydration correction would still jump there. Restructure instead: the nav now sits in normal document flow inside a .menu-band wrapper, so the orange band is exactly as tall as the menu content in every locale and at every viewport width. This removes the JS measurement + ResizeObserver and all hardcoded band heights; the photo, scrim and slogan paddings no longer need the --menu-orange offset. Side effect: the band is ~5px taller than the JS-measured value used to be (the nav's own box includes child margins the measurement excluded). Verified: photo-top/hero rects identical across pre-hydration, hydrated, fallback-font and webfont states at 1280/700/375px.
The in-flow band was ~5px taller than the old JS-measured value because the nav's children carry a 0.25rem wrap margin that now lands inside the band. Compensate in the band nav's bottom padding so the menu sits optically centred and the band height matches production (142/180/213px at desktop/tablet/phone). Also remove rules the restructure made redundant: the hero's 100vw/translateX full-bleed trick (its parent is already full-bleed, and 100vw overflows by the scrollbar width) and the hero-section's position: relative (nothing anchors to it anymore).
- Give .menu-band position:relative + z-index:1 so the language
switcher dropdown (absolute, opens downward past the band) paints
above the .hero sibling on multi-locale builds. The old absolute
nav carried z-index:1; the in-flow restructure had lost that.
Remove the now-ineffective z-index from .inverted-header (static
position ignores it).
- Replace the navbar's nav > * { margin-bottom: 0.25rem } wrap-gap
hack with row-gap on the nav itself. This makes the band's
padding compensation unnecessary (band heights unchanged:
142/180/213) and drops 4px of stray trailing space from nav boxes
on all pages.
- Name the shared hero orange (--hero-orange) instead of duplicating
#ff9416 in the band and the slogan backdrop.
- Add postcss-discard-duplicates: fontaine emits one fallback
@font-face per src URL x unicode-range subset, all metric-identical
copies; deduping collapses 186 rules to 15 and shrinks the
render-blocking layout CSS from 77K to 44K.
- Drop position:relative on .hero (nothing anchors to it).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the layout jump on home page load: the orange menu band grows shortly after first paint, pushing the hero photo and everything below it down.
Causes
Two stacked issues, measured with a Playwright probe that samples element rects across the load (the numbers below are the measured top of the hero photo, i.e. the orange band height, at a 1280px viewport):
--menu-orangehydration mismatch — the jump you see with a warm font cache. The orange band behind the menu defaults to 128px (desktop) / 280px (mobile) in CSS, but after hydration JS measures the real nav and sets 142px / 180px / 213px (depending on menu wrapping). The nav itself is absolutely positioned at the top, so it doesn't move — the band edge, photo and all content below jump down 14px on desktop (and up 67px on small mobile).font-display: swap, so the first paint uses fallback fonts (Impact/serif) whose metrics differ a lot from Saira Condensed and Roboto Slab. The band goes 128px → 171px (hydration measures the fallback-font nav) → 142px (webfonts arrive, re-measure), with all hero/nav text reflowing along the way. Preloading doesn't prevent this — paint isn't blocked on fonts.Fix
.menu-bandwrapper instead of being absolutely positioned and measured by JS. The band is therefore exactly as tall as the menu content — in every locale and at every viewport width — and the JS measurement, theResizeObserver, and all hardcoded band heights are gone. The band height matches what production renders after its JS correction (142/180/213px at desktop/tablet/phone)."<font> fallback"@font-facerules whosesize-adjust/ascent-override/descent-overridemetrics match the webfonts, and the--font-body/--font-headingvariables now include them. The fallback lists include Tinos, Arimo and Noto alongside Georgia/Times/Arial sosrc: local()also resolves on Linux, where the Microsoft fonts don't exist.Notable details:
.menu-bandcarriesposition: relative; z-index: 1so the language-switcher dropdown (rendered on multi-locale builds, opens downward past the band) paints above the hero — the old absolute nav hadz-index: 1, and the in-flow restructure would otherwise have lost that.margin-bottomtorow-gapon the nav; nav boxes on all pages lose 4px of stray trailing space the old hack added.@font-faceper src URL × unicode-range subset — 186 metric-identical copies.postcss-discard-duplicates(wired into Vite'scss.postcss) collapses them to the 15 unique rules, shrinking the render-blocking layout CSS from 77K to 44K.width: 100vw+translateX(-50%)full-bleed trick is gone (its parent is already full-bleed, and100vwoverflowed by the scrollbar width), and the shared hero orange is named--hero-orange.Before
Jump 1 — hydration band correction (fonts cached; this is the commonly seen jump). Nav stays put, the orange band grows 14px and the photo + content shift down:
Jump 2 — font swap (cold cache; webfonts blocked to hold the fallback state). Text reflows and the band overshoots to 171px before settling at 142px:
After
The photo edge sits at the same position in every state — pre-hydration, hydrated, fallback fonts, webfonts. Hardest case shown (cold cache): the fallback glyphs differ slightly, but nothing moves when the webfonts swap in:
Verification
128 → 142(warm cache) and128 → 171 → 142(cold cache); after: constant in all four states (142px at 1280px, 180px at 700px, 213px at 375px — matching production's settled values), with hero rects identical across the font swap at all three viewports.pnpm checkpasses;pnpm buildsucceeds, the built CSS contains the generated fallback rules, and the duplicate-rule dedupe is confirmed in the output (186 → 15 fallback@font-facerules).pnpm check+pnpm lint) passes.