From 7e473e48d0092f3d50d22c0b2eed16f77171347f Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 22 Apr 2026 10:57:24 -0700 Subject: [PATCH] fix(site): instant hash restore, retry on fonts.ready + load, drop smooth-scroll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related site scroll bugs: 1. initAnchorScroll and initHashTracking both called scrollTo with `behavior: 'auto'`, which defers to CSS `scroll-behavior`. Because sub-pages.css set `html { scroll-behavior: smooth }`, every anchor jump and reload-hash-restore animated — despite a code comment explicitly stating "Instant anchor scroll — no smooth scrolling". Switch to `behavior: 'instant'` so the JS wins. 2. The reload-hash restore used a fixed `setTimeout(100)` to compute target position. At 100ms, async Google Fonts (Cormorant Garamond italic) has not swapped in, so `getBoundingClientRect().top` is computed against fallback metrics and mislanded by hundreds of pixels. Retry on `document.fonts.ready` and on window `load`. 3. Remove `scroll-behavior: smooth` from sub-pages.css entirely — it was silently fighting the JS and made long-page anchor clicks feel sluggish. --- public/css/sub-pages.css | 5 ++--- public/js/utils/scroll.js | 28 ++++++++++++++++++---------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/public/css/sub-pages.css b/public/css/sub-pages.css index c4be084bd..cde466469 100644 --- a/public/css/sub-pages.css +++ b/public/css/sub-pages.css @@ -37,9 +37,8 @@ BASE ============================================ */ -html { - scroll-behavior: smooth; -} +/* No smooth scroll — long editorial pages feel slow on anchor jumps, and + a secretly-smooth CSS rule also overrides JS `behavior: 'auto'` calls. */ body { font-family: var(--font-body); diff --git a/public/js/utils/scroll.js b/public/js/utils/scroll.js index ebe48b5a3..2e149e86f 100644 --- a/public/js/utils/scroll.js +++ b/public/js/utils/scroll.js @@ -1,14 +1,15 @@ -// Instant anchor scroll - no smooth scrolling for better UX on long pages +// Instant anchor scroll - no smooth scrolling for better UX on long pages. +// `behavior: 'instant'` explicitly overrides any CSS `scroll-behavior: smooth` +// from a stylesheet we don't own; `behavior: 'auto'` would defer to CSS. export function initAnchorScroll() { document.querySelectorAll('a[href^="#"]').forEach((anchor) => { anchor.addEventListener("click", (e) => { e.preventDefault(); const target = document.querySelector(anchor.getAttribute("href")); if (target) { - // Instant jump with small offset for visual breathing room const offset = 40; const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset; - window.scrollTo({ top: targetPosition, behavior: 'auto' }); + window.scrollTo({ top: targetPosition, behavior: 'instant' }); } }); }); @@ -75,22 +76,29 @@ export function initHashTracking() { } }, { passive: true }); - // Handle initial hash on page load - instant jump + // Handle initial hash on page load — instant jump, retried on + // fonts.ready and window `load`. A fixed setTimeout is unreliable + // because async-loaded display fonts reflow the page by hundreds of + // pixels when they swap in; computing target position before that + // lands the user several sections above the right spot. if (window.location.hash) { const hash = window.location.hash.slice(1); const target = document.getElementById(hash); if (target) { currentHash = hash; - setTimeout(() => { + let clicked = false; + const jump = () => { const offset = 40; const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset; - window.scrollTo({ top: targetPosition, behavior: 'auto' }); - - // If it's a command deep link, activate it - if (hash.startsWith('cmd-') && target.classList.contains('manual-entry')) { + window.scrollTo({ top: targetPosition, behavior: 'instant' }); + if (!clicked && hash.startsWith('cmd-') && target.classList.contains('manual-entry')) { target.click(); + clicked = true; } - }, 100); + }; + jump(); + if (document.fonts?.ready) document.fonts.ready.then(jump).catch(() => {}); + window.addEventListener('load', jump, { once: true }); } } else { // No hash — don't set one on initial load