Skip to content

fix: useTheme now subscribes to live OS theme changes in 'system' mode#34

Merged
abayomicornelius merged 4 commits into
StellarSend:mainfrom
circleboyslimited:fix/usetheme-system-live-updates
Jul 17, 2026
Merged

fix: useTheme now subscribes to live OS theme changes in 'system' mode#34
abayomicornelius merged 4 commits into
StellarSend:mainfrom
circleboyslimited:fix/usetheme-system-live-updates

Conversation

@circleboyslimited

Copy link
Copy Markdown
Contributor

Summary

Fixes #31.

useTheme read matchMedia('(prefers-color-scheme: dark)').matches once, inside an effect keyed only on theme, so the <html> element's dark class never updated again unless theme itself changed. A user on 'system' whose OS switched between light/dark (time-of-day auto-switching, or a manual OS toggle) while the app stayed open would get visually stuck on whichever mode was active at mount or at the last explicit theme change — invisible in normal manual QA, very visible to real users.

The fix already existed one file away: useIsDarkMode() (src/hooks/useMediaQuery.ts) correctly subscribes to matchMedia's change event via useMediaQuery. useTheme just wasn't using it — it re-implemented a one-shot, non-reactive version of the same check inline. Swapped the inline check for useIsDarkMode() and added its value to the effect's dependency array, so an OS-level change now re-triggers the effect (and only affects the dark class) when theme === 'system'.

const systemIsDark = useIsDarkMode()
useEffect(() => {
  const root = document.documentElement
  if (theme === 'dark') root.classList.add('dark')
  else if (theme === 'light') root.classList.remove('dark')
  else root.classList.toggle('dark', systemIsDark)
}, [theme, systemIsDark])

While in there, I noticed useMediaQuery/useIsDarkMode — which useTheme's correctness now directly depends on — had zero test coverage of its own, so I added that too.

Test plan

  • src/hooks/useTheme.test.ts (new): reproduces the issue's exact repro steps with a mocked matchMedia whose change handler is fired mid-test — asserts the dark class updates in both directions (light→dark, dark→light) with no change to theme itself, and regression-tests that explicit 'light'/'dark' selections are unaffected by OS-level changes
  • src/hooks/useMediaQuery.test.ts (new): initial synchronous value, reacts to a change event, and removes its listener on unmount
  • npx vitest run — 74/74 passing
  • npm run lint — clean
  • npx tsc --noEmit — clean
  • npm run build — succeeds

useTheme read matchMedia('(prefers-color-scheme: dark)').matches once,
inside an effect keyed only on `theme`, so the <html> dark class never
updated again unless `theme` itself changed. A user on 'system' whose
OS switched between light/dark (time-of-day auto-switching, or a
manual OS toggle) while the app stayed open would get stuck on
whichever mode was active at mount/last theme change.

useIsDarkMode() (src/hooks/useMediaQuery.ts) already solves this
correctly via matchMedia's 'change' event — useTheme just wasn't
calling it. Swap the inline one-shot check for useIsDarkMode() and add
its value to the effect's dependency array, so OS-level changes now
also trigger a re-toggle when (and only when) theme === 'system'.
Covers the exact reproduction from the issue: with theme === 'system',
firing a mocked matchMedia 'change' event now flips the dark class
without theme itself changing (both directions, light->dark and
dark->light). Regression-tests that explicit 'light'/'dark' selections
are unaffected by OS-level changes — only 'system' should react.
useTheme's correctness now depends on this hook actually re-firing on
'change' events, and it had no direct test coverage at all. Covers
the initial synchronous value, reacting to a change event, and that
the change listener is removed on unmount (no dangling setState after
unmount).
@circleboyslimited

Copy link
Copy Markdown
Contributor Author

All checks has passed. kindly review

@abayomicornelius
abayomicornelius merged commit 0eb2921 into StellarSend:main Jul 17, 2026
4 checks passed
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.

useTheme doesn't subscribe to OS theme changes when set to 'system', unlike the sibling useMediaQuery hook that already solves this correctly

2 participants