fix: eliminate header UserMenu skeleton layout shift #79
Inga granskare
Etiketter
Inga etiketter
area/backups
area/ci
area/control-panel
area/identity
area/infra
area/observability
area/payments
area/security
area/storage
area/web
blocked
needs-info
needs-triage
ready-for-implementation
type
bug
type
chore
type
docs
type
epic
type
feature
type
task
wontfix
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Inget förfallodatum satt.
Beroenden
Inga beroenden satta
Referens
bitborg/bitborg-web!79
Läser in…
Hänvisa till i nytt ärende
Ingen beskrivning angiven.
Ta bort grenen "fix/78-header-skeleton-cls"
Borttagning av en gren är permanent. Även om den borttagna grenen kan fortsätta existera en kort tid innan den faktiskt tas bort, kan det INTE ångras i de flesta fall. Vill du fortsätta?
Root cause
The
<UserMenu server:defer />fallback skeleton (added as #65 M9 to prevent header CLS) itself caused a layout shift when the island streamed in.The resolved
<nav class="user-menu">is a flex row withalign-items: center,gap: var(--spacing-md)and no padding / border / margin / border-radius, so its cross-size is simply its tallest child — an.icon-linkwhose icon issize="1lh". That makes the resolved menu exactly one line box tall (1lh ≈ 21.3px).The fallback, however, forced
min-block-size: var(--min-touch-target)(3rem = 48px) with placeholder bars ofblock-size: 1rem. So the reserved box was 48px while the real menu is 21.3px — the header row collapsed by ~27px vertically the momentserver:deferswapped the island in.(The gap already matched
--spacing-md, and neither box has padding/border/radius, so height was the sole real mismatch. The language switcher is pinned right byjustify-content: space-between, so user-menu width cannot shift it horizontally — width is not a CLS factor here.)The fix
Match the fallback's outer box to the resolved menu by construction, using shared sizing rather than magic numbers:
min-block-size: var(--min-touch-target)(the real nav has none).block-size: 1lh— the same line box the real.icon-linkicons (size="1lh") use. This tracks the font-size shrink at the small breakpoint automatically, since both the real icons and the bars scale with the line box.display: flex; align-items: center; gap: var(--spacing-md)(unchanged — already matched the resolved nav).Still a subtle placeholder (muted bars at 0.5 opacity); no shimmer animation, so no reduced-motion handling needed.
Anonymous visitors (who see the skeleton to content swap most) render two links — sign in + sign up — matching the two placeholder bars.
Verification (CLS approximately 0)
Measured in headless Chromium (Edge 150) via the DevTools Protocol.
1. Direct box measurement (standalone harness with the real token values):
.user-menu2. Live page CLS (built server, throttled network so the fallback actually paints before the swap,
PerformanceObserver('layout-shift'), 1280x800):pnpm check,pnpm lint,pnpm buildall clean.Closes #78
45f82da7943f01e985ad