Move Theme State to URL - #102
Merged
Merged
Conversation
romanaduraciova
approved these changes
Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
What was done
1. Theme state moved to URL params (
?theme=eosc&mode=dark)The theme switcher state is now shareable via URL. Precedence: URL param > localStorage > hardcoded default. When you switch themes, the URL updates via
router.replace(no full navigation) and localStorage is kept in sync as a cache. Browser back/forward re-syncs the theme from the URL.Files:
src/hooks/use-theme-switcher.ts(URL read/write logic),src/components/ThemeSelector.tsx(Suspense boundary foruseSearchParams),src/app/layout.tsx(inline FOUC script extended to read URL params).2. next-themes removed entirely
After auditing all theme-related files, next-themes was found to be strictly redundant: it only managed the
.darkclass (which the custom system already handled), used a separate localStorage key ("theme"vs"theme-color-mode"), had no concept of branding (data-theme), and no URL param support. The only sync point was a fragile manual dual-call inThemeSelector(setDefaultTheme(mode)+setTheme(mode)), and any theme change bypassing that call would silently desync the two systems.Files deleted:
src/components/ThemeProvider.tsx,src/components/ThemeInitializer.tsx(the latter was also redundant with the inline FOUC script and contained an unfixeddataset.Themecapitalization bug). next-themes removed frompackage.jsonpeerDeps andvite.config.lib.tsexternals.3. Sonner made theme-agnostic
The library's
Toastercomponent was the only library file importing next-themes (to readuseTheme().themefor toast styling). It now accepts an optionalthemeprop instead, so the library no longer forces consumers to install next-themes.File:
lib/components/primitives/sonner.tsx.Why it was needed
The original codebase had four competing mechanisms managing the
.darkclass using two different localStorage keys, with a single manual call inThemeSelectorkeeping them aligned. Adding URL param support on top of that would have made the desync problem worse — URL-triggered theme changes (shared links, browser navigation) bypassThemeSelectorentirely, so next-themes would go stale while the custom system stayed correct. Removing next-themes eliminated the entire class of sync bugs and simplified the architecture to one system, one localStorage key set, and one FOUC script.Closes #76