From 5aa2e4589e69c1fda5f9226eca7099fb59f9df94 Mon Sep 17 00:00:00 2001 From: austin Date: Sun, 26 Jul 2026 22:07:52 -0500 Subject: [PATCH] The site stops flashing white on every click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a nav link or a filter chip made the whole site look like it reloaded, because it did: body carried `opacity: 0` and a half-second fadeIn, so every navigation went blank, then slid 10px and faded back. Those two lines are the SPA's. There they ran once, when React booted, and every navigation afterwards happened inside a document that had already faded in. Server-rendered, every link is a new document, so a once-per-session animation became a once-per-click one — and the thing it was decorating was already fast. Replaced with the cross-document view transition it was reaching for: the browser holds the old page up until the new one is ready and cross-fades, so nothing is ever blank. Where it isn't supported the navigation is simply immediate, which is the right fallback and still better than a blank page. Reduced motion keeps the navigation and drops the cross-fade; the print rule no longer has to undo an opacity that isn't set any more. Co-Authored-By: Claude Opus 5 (1M context) --- src/main/styles/base.css | 41 +++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/main/styles/base.css b/src/main/styles/base.css index dc3ddbb..0d6493b 100644 --- a/src/main/styles/base.css +++ b/src/main/styles/base.css @@ -1,6 +1,6 @@ /* - * Element defaults: the page background, the body type, the fade-in, and the two small resets the site - * needs. Anything with a class name belongs in components.css instead. + * Element defaults: the page background, the body type, how one page gives way to the next, and the two + * small resets the site needs. Anything with a class name belongs in components.css instead. */ html { @@ -14,23 +14,32 @@ body { color: var(--color-bakery-900); font-family: var(--font-sans); overflow-x: hidden; - opacity: 0; - animation: fadeIn 0.5s ease-in forwards; +} + +/* + * BETWEEN PAGES. + * + * The body used to open at `opacity: 0` and fade in over half a second on every page. That belonged to + * the SPA, where it ran ONCE, when React booted — the rest of the site's navigation happened inside a + * document that had already faded in. Server-rendered, every nav link and every filter chip is a new + * document, so the same two lines fired on every click: the page went blank, then slid 10px and faded + * back. A navigation that was already fast read as the whole site reloading. + * + * A cross-document view transition is what that fade was reaching for. The browser holds the old page + * up until the new one is ready and cross-fades between the two, so nothing is ever blank — and where + * it isn't supported you get a plain, immediate page load, which is the correct fallback and is still + * better than what this replaced. + */ +@view-transition { + navigation: auto; } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - body { animation: none; opacity: 1; } -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); + /* Cut the cross-fade, keep the navigation. */ + ::view-transition-old(root), + ::view-transition-new(root) { + animation: none; } } @@ -109,8 +118,6 @@ select.field { body { background: #fff; color: #000; - opacity: 1; - animation: none; } /* Sage-on-white blocks become plain text: the words matter, the ink does not. */