Skip to content

Header: font load#73389

Open
breville wants to merge 2 commits into
stagingfrom
header-font-load
Open

Header: font load#73389
breville wants to merge 2 commits into
stagingfrom
header-font-load

Conversation

@breville

@breville breville commented Jun 22, 2026

Copy link
Copy Markdown
Member

[written by Claude]

Stabilize the code-studio header across the webfont swap.

Summary

Early in page load the code-studio header shifts horizontally by a few pixels: icons (help ?, the user/create menu carets) pop as their glyphs load, and the centered lesson title and progress bubbles drift as the text font swaps in. The cause is webfonts arriving after first paint. This PR does two distinct things, without changing the header's final, fully-loaded appearance:

  • Removes the icon-driven shift outright — the reservations hold the layout across the icon-font load, so the glyphs fill already-reserved space and nothing moves, for users and eyes alike.
  • Stabilizes the rest for the eyes tests — the text-font reflow of the centered content still happens on a cold load; the re-measure makes it land in its correct final position (rather than a stale one) and lets the eyes capture wait for that settled layout, so the tests stop flagging spurious "font wiggle." Preventing that text reflow for users too is a separate, optional font-loading change (see Notes).

Background

The header lays out its middle section in JavaScript: each piece (lesson title, progress, project info, finish link) measures its own rendered width and reports it up, and HeaderMiddle uses those widths to allocate space and center the content. That measuring logic has been in place for years.

In the time since, two things changed around it:

  • The header text font migrated more than once (Gotham → Metropolis → Figtree), and today loads as a webfont with font-display: swap.
  • Eyes tests grew font-load waits to fight "font wiggle."

Each was reasonable on its own. The interaction is what bites: the measuring code runs on mount — which on a cold load happens before the webfont arrives — and has no trigger to re-measure when a font finishes loading (it re-measures on window resize/scroll, but a font swap fires neither). So it measures against the fallback font and bakes those widths into the centering, where they persist unless some unrelated event re-measures.

The icons are a separate, older thread. The header has rendered Font Awesome icons since 2017, so the root behavior — an icon-font glyph has near-zero width until the font loads, then snaps to full size and shoves its neighbors when nothing reserves its box — is longstanding, not something introduced recently. (The icon class references were modernized from FA v4 to v6/v7 syntax in March 2026, but that doesn't change the collapse: an unreserved glyph has no width until its font arrives, regardless of version.) A latent issue all along, just easy to miss.

Together these produce a small, racy horizontal shift during load and intermittent eyes diffs — none of it visible once everything is cached/loaded.

What changed

1. Reserve the icon glyph boxes (CSS). The Font Awesome icons in the header had no reserved width, so they collapsed before the icon font loaded and shoved their neighbors when it arrived. Each now reserves a fixed box equal to its glyph advance, so the layout holds across the font swap:

  • help icon ? — 22px
  • user-menu chevron — 14px
  • create-menu plus — 12.25px

Because each width equals the glyph's own advance, the loaded appearance is unchanged. (The hamburger already reserved its box; the "MORE" caret sits in a fixed-width slot; the progress-bubble tooltip icons are off-screen — all left alone.)

2. Re-measure the header once fonts load (JS). HeaderMiddle and its measuring children now re-measure on document.fonts.ready — which resolves only after the in-use webfonts (both Figtree and Font Awesome) have loaded and laid out — and correct the centering that was computed against the fallback font. .header_middle is the flex remainder after the content-sized side columns, so its width also moves as those columns reflow; it is re-measured too.

3. Make eyes captures deterministic (UI tests). The eyes wait used document.fonts.status, which reads "loaded" whenever nothing is currently loading — true even before the page's fonts are requested. It now awaits the document.fonts.ready promise instead, plus a data-header-fonts-relaid-out flag the header sets after its re-measure, so a capture lands on the final layout. Pages that don't render this header (no .header_middle) pass straight through and are unaffected.

Verification

Measured locally in a real browser (document.fonts driven to the fallback state and back):

  • Help icon: collapse 14px → 0. Reserved width equals the glyph advance exactly.
  • User/create menu buttons: each collapse 4.7px → 0.
  • Caret loaded form: chevron {w:14, top:59.5, h:14} and plus {w:12.25, top:17.5, h:14} are pixel-identical with and without the reservation.
  • Measure-once staleness: with the JS measuring against the fallback font, the centered middle content sits ~1.5px off the Figtree-measured position; the fonts.ready re-measure snaps it to the correct column.
  • document.fonts.ready confirmed to resolve with Font Awesome 6 Pro (900) loaded, not just the text font.

Effect on eyes baselines

The PR is intended to be final-form-neutral. The icon reservations match each glyph's advance, so the loaded layout is unchanged and they should not move baselines. The only change that can move a baseline is the re-measure, and only where an existing baseline was captured in the stale (fallback-measured) state — in which case the new capture reflects the correct layout. Expect any baseline deltas to be small (~1–2px) and to represent the header settling where it always should have.

Notes

  • Tagged for Chrome-only CI (no [test all browsers]).
  • Not included, optional follow-ups for the user-facing cold-load reflow (as opposed to the eyes determinism this PR targets): font-display: optional on the header font, or a metric-matched fallback. Left out deliberately; happy to file a follow-up if we want the cold-load experience fully reflow-free.

breville and others added 2 commits June 22, 2026 19:06
The code-studio header shifts horizontally early in page load as the
Figtree text webfont and the Font Awesome icon webfont replace their
fallbacks. Three independent causes, fixed here:

- The help icon (.help_icon) reserves no width, so its Font Awesome glyph
  is zero-width until the font loads and then pops to 22px, shoving the
  adjacent user menu. Reserve a 22px glyph box; 22px matches the glyph
  advance, so the layout holds across the swap.

- HeaderMiddle and its children measure text widths on mount, which on a
  cold load precedes the webfont, and never re-measure. The fallback
  font's widths then persist in the centering math (.header_middle is the
  flex remainder, so its width also moves as the side columns reflow).
  Re-measure on document.fonts.ready.

- Eyes UI tests waited on document.fonts.status, which reads "loaded"
  whenever nothing is currently loading -- true before the page's fonts
  are even requested. Await the readiness promise instead, and a
  data-header-fonts-relaid-out flag the header sets after re-measuring, so
  captures land on the final layout. Pages without this header pass
  straight through and are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The signed-in user-menu chevron and create-menu plus are Font Awesome
glyphs with no reserved width, so each collapses before the webfont loads
and then expands, shifting the help icon and hamburger beside it ~4.7px.
Reserve each glyph's box at its own advance (chevron 14px, plus 12.25px),
the same fix the help icon got; the loaded layout is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@breville breville added the AI generated This PR has been substantially generated using AI. label Jun 22, 2026
@breville breville changed the title Header font load Header: font load Jun 22, 2026

@molly-moen molly-moen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI generated This PR has been substantially generated using AI.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants