Skip to content

fix: honor system theme on homepage first paint - #30

Draft
wustep wants to merge 2 commits into
mainfrom
cursor/homepage-theme-first-paint-894f
Draft

fix: honor system theme on homepage first paint#30
wustep wants to merge 2 commits into
mainfrom
cursor/homepage-theme-first-paint-894f

Conversation

@wustep

@wustep wustep commented Aug 17, 2026

Copy link
Copy Markdown
Owner

The homepage (~60% of weekly visitors) keeps its own isDark state, defaults it to true, and writes that to localStorage on mount. Light-mode visitors therefore:

  1. Get a dark first paint (<main class="…dark"> in the SSR HTML)
  2. Flash to light after hydration
  3. Briefly overwrite a saved light preference with true

Journal / philosophy pages already use useDarkMode + the inline noflash script on body.dark-mode. The homepage ignored both.

What changed

  • Drive About page tokens from body.dark-mode (set by the existing noflash script before first paint)
  • Use the shared useDarkMode hook for the theme toggle so homepage and Notion pages stay in sync
  • Swap the Notion mark and Lenses illustration via CSS (--about-lenses-*, .iconOnDark / .iconOnLight) so they don’t wait on React state

No brand redesign — same light and dark palettes, just applied at the right time.

Why

Light-preferring visitors (and anyone with darkMode=false saved) were paying a dark flash on the most-viewed page. Toggling theme on the homepage also failed to update body.dark-mode, so a client navigation to a journal page could disagree with the homepage.

Before / after

Before (production first paint): SSR hardcodes the dark class. Mid-load the dark page is fading in over a light body, which reads as washed-out low-contrast chrome:

Production homepage first paint — washed out dark-on-light flash

Before (production after hydration, light preference): the page does land on light — the bug is the flash getting there, plus the independent theme state.

Production homepage after JS hydration in light mode

After (this branch, light preference — first paint is already light):

Local homepage light mode desktop

Local homepage light mode mobile

After (this branch, dark preference — first paint is already dark):

Local homepage dark mode desktop

Local homepage dark mode mobile

The circular “N” in the corner of the local shots is the Next.js 16 dev indicator, not site chrome.

Test plan

  • Fresh profile, prefers-color-scheme: light: homepage is light on first paint (no dark flash)
  • Fresh profile, prefers-color-scheme: dark: homepage is dark on first paint (no light flash)
  • Toggle theme on /, then open /philosophy: both pages match
  • Toggle theme on /philosophy, then go home: homepage matches
  • Reload / after choosing light: stays light (localStorage not overwritten to true)
  • Notion mark and Lenses eye recolor with the theme (no hydration pop)
  • Mobile (390px): header, experience rows, and project cards still layout correctly in both themes
  • prefers-reduced-motion: no entrance animation; theme still correct

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

The about page hardcoded dark on SSR and only flipped after hydration,
so light-mode visitors (and the noflash body class used everywhere else)
got a dark flash. Drive tokens from body.dark-mode and use the shared
useDarkMode hook so the toggle stays in sync with journal pages.

Co-authored-by: Stephen Wu <wustep@users.noreply.github.com>
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
me Ready Ready Preview Aug 17, 2026 5:33am

Request Review

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants