1- " use client" ;
1+ ' use client' ;
22
3- import styles from "./switch.module.css" ;
4- import { memo , useEffect , useState } from "react" ;
3+ import { memo , useEffect , useState } from 'react' ;
4+ import styles from './switch.module.css' ;
55
6- declare global {
7- var updateDOM : ( ) => void ;
8- }
6+ const STORAGE_KEY = 'this-theme' ;
7+ const MODES = [ 'system' , 'dark' , 'light' ] as const ;
8+ type ColorSchemePreference = typeof MODES [ number ] ;
99
10- type ColorSchemePreference = "system" | "dark" | "light" ;
11-
12- const STORAGE_KEY = "this-theme" ;
13- const modes : ColorSchemePreference [ ] = [ "system" , "dark" , "light" ] ;
14-
15- /** to reuse updateDOM function defined inside injected script */
16-
17- /** function to be injected in script tag for avoiding FOUC (Flash of Unstyled Content) */
18- const NoFOUCScript = ( storageKey : string ) => {
19- /* can not use outside constants or function as this script will be injected in a different context */
20- const [ SYSTEM , DARK , LIGHT ] = [ "system" , "dark" , "light" ] ;
10+ // Injects theme script before hydration to avoid FOUC
11+ const noFOUCScript = ( storageKey : string ) => {
12+ const [ SYSTEM , DARK , LIGHT ] = [ 'system' , 'dark' , 'light' ] ;
13+ const media = matchMedia ( `(prefers-color-scheme: ${ DARK } )` ) ;
2114
22- /** Modify transition globally to avoid patched transitions */
23- const modifyTransition = ( ) => {
24- const css = document . createElement ( "style" ) ;
25- css . textContent = "*,*:after,*:before{transition:none !important;}" ;
26- document . head . appendChild ( css ) ;
15+ const disableTransitionsTemporarily = ( ) => {
16+ const style = document . createElement ( 'style' ) ;
17+ style . textContent = '*, *::before, *::after { transition: none !important; }' ;
18+ document . head . appendChild ( style ) ;
2719
2820 return ( ) => {
29- /* Force restyle */
3021 getComputedStyle ( document . body ) ;
31- /* Wait for next tick before removing */
32- setTimeout ( ( ) => document . head . removeChild ( css ) , 1 ) ;
22+ setTimeout ( ( ) => document . head . removeChild ( style ) , 1 ) ;
3323 } ;
3424 } ;
3525
36- const media = matchMedia ( `(prefers-color-scheme: ${ DARK } )` ) ;
37-
38- /** function to add remove dark class */
39- window . updateDOM = ( ) => {
40- const restoreTransitions = modifyTransition ( ) ;
41- const mode = localStorage . getItem ( storageKey ) ?? SYSTEM ;
26+ const applyTheme = ( ) => {
27+ const restoreTransitions = disableTransitionsTemporarily ( ) ;
28+ const storedMode = localStorage . getItem ( storageKey ) ?? SYSTEM ;
4229 const systemMode = media . matches ? DARK : LIGHT ;
43- const resolvedMode = mode === SYSTEM ? systemMode : mode ;
30+ const resolvedMode = storedMode === SYSTEM ? systemMode : storedMode ;
31+
4432 const classList = document . documentElement . classList ;
45- if ( resolvedMode === DARK ) classList . add ( DARK ) ;
46- else classList . remove ( DARK ) ;
47- document . documentElement . setAttribute ( "data-mode" , mode ) ;
33+ classList . toggle ( DARK , resolvedMode === DARK ) ;
34+ document . documentElement . setAttribute ( 'data-mode' , storedMode ) ;
4835 restoreTransitions ( ) ;
4936 } ;
50- window . updateDOM ( ) ;
51- media . addEventListener ( "change" , window . updateDOM ) ;
52- } ;
5337
54- let updateDOM : ( ) => void ;
38+ applyTheme ( ) ;
39+ media . addEventListener ( 'change' , applyTheme ) ;
40+ } ;
5541
56- /**
57- * Switch button to quickly toggle user preference.
58- */
5942const Switch = ( ) => {
60- const [ mode , setMode ] = useState < ColorSchemePreference > (
61- ( ) =>
62- ( ( typeof localStorage !== "undefined" &&
63- localStorage . getItem ( STORAGE_KEY ) ) ??
64- "system" ) as ColorSchemePreference ,
65- ) ;
43+ const [ mode , setMode ] = useState < ColorSchemePreference > ( ( ) => {
44+ return ( typeof localStorage !== 'undefined'
45+ ? ( localStorage . getItem ( STORAGE_KEY ) as ColorSchemePreference )
46+ : 'system' ) ?? 'system' ;
47+ } ) ;
6648
6749 useEffect ( ( ) => {
68- // store global functions to local variables to avoid any interference
69- updateDOM = window . updateDOM ;
70- /** Sync the tabs */
71- addEventListener ( "storage" , ( e : StorageEvent ) : void => {
72- e . key === STORAGE_KEY && setMode ( e . newValue as ColorSchemePreference ) ;
73- } ) ;
50+ const storedMode = localStorage . getItem ( STORAGE_KEY ) as ColorSchemePreference ;
51+ setMode ( storedMode ?? 'system' ) ;
7452 } , [ ] ) ;
7553
7654 useEffect ( ( ) => {
7755 localStorage . setItem ( STORAGE_KEY , mode ) ;
78- updateDOM ( ) ;
56+ const systemMode = matchMedia ( '(prefers-color-scheme: dark)' ) . matches ? 'dark' : 'light' ;
57+ const resolvedMode = mode === 'system' ? systemMode : mode ;
58+ const classList = document . documentElement . classList ;
59+ classList . toggle ( 'dark' , resolvedMode === 'dark' ) ;
60+ document . documentElement . setAttribute ( 'data-mode' , mode ) ;
7961 } , [ mode ] ) ;
8062
81- /** toggle mode */
82- const handleModeSwitch = ( ) => {
83- const index = modes . indexOf ( mode ) ;
84- setMode ( modes [ ( index + 1 ) % modes . length ] ) ;
63+ const handleToggle = ( ) => {
64+ const nextIndex = ( MODES . indexOf ( mode ) + 1 ) % MODES . length ;
65+ setMode ( MODES [ nextIndex ] ) ;
8566 } ;
67+
8668 return (
8769 < button
88- suppressHydrationWarning = { true }
70+ suppressHydrationWarning
8971 className = { styles . switch }
90- aria-label = "ThemeSwitch"
91- onClick = { handleModeSwitch }
92- >
93- </ button >
72+ aria-label = "Toggle theme"
73+ onClick = { handleToggle }
74+ />
9475 ) ;
9576} ;
9677
9778const Script = memo ( ( ) => (
9879 < script
80+ suppressHydrationWarning
9981 dangerouslySetInnerHTML = { {
100- __html : `(${ NoFOUCScript . toString ( ) } )('${ STORAGE_KEY } ')` ,
82+ __html : `(${ noFOUCScript . toString ( ) } )('${ STORAGE_KEY } ')` ,
10183 } }
10284 />
10385) ) ;
10486
105- /**
106- * This component wich applies classes and transitions.
107- */
108- export const ThemeSwitcher = ( ) => {
109- return (
110- < >
111- < Script />
112- < Switch />
113- </ >
114- ) ;
115- } ;
87+ export const ThemeSwitcher = ( ) => (
88+ < >
89+ < Script />
90+ < Switch />
91+ </ >
92+ ) ;
0 commit comments