24
loading...
This website collects cookies to deliver better user experience
<meta id="colorScheme" name="color-scheme" content="light dark" />
light
and dark
, and I'm updating 2 css variables, than in the end control the look of the main body:body.light {
--color: #111;
--background: #fff;
}
body.dark {
--color: #cecece;
--background: #333;
}
body {
color: var(--color);
background: var(--background);
}
<script>
const mode = localStorage.getItem("mode") || "system";
let theme;
if (mode === "system") {
const isSystemInDarkMode = matchMedia("(prefers-color-scheme: dark)")
.matches;
theme = isSystemInDarkMode ? "dark" : "light";
} else {
// for light and dark, the theme is the mode
theme = mode;
}
document.body.classList.add(theme);
</script>
mode
for the saved modes (light / dark / system), and theme
for the visual themes (light / dark):// Saved mode
type Mode = "light" | "dark" | "system";
// Visual themes
type Theme = "light" | "dark";
const ThemeContext = React.createContext<{
mode: Mode;
theme: Theme;
setMode: (mode: Mode) => void;
}>({
mode: "system",
theme: "light",
setMode: () => {}
});
React.useState
, you can provide a function, called a lazy initial state, that will only get called during the 1st render:const [mode, setMode] = React.useState<Mode>(() => {
const initialMode =
(localStorage.getItem(localStorageKey) as Mode | undefined) || "system";
return initialMode;
});
mode
state, we need to update it with the remote database. To do so, we could use an effect, but I decided to use another useState
, which seems weird as I'm not using the returned state, but as mentioned above, lazy initial states are only called during the 1st render.// This will only get called during the 1st render
React.useState(() => {
getMode().then(setMode);
});
mode
in the dependencies array, so that the effect will be called every time the mode changes:React.useEffect(() => {
localStorage.setItem(localStorageKey, mode);
saveMode(mode); // database
}, [mode]);
system
mode with the theme users picked for their devices:const [theme, setTheme] = React.useState<Theme>(() => {
if (mode !== "system") {
return mode;
}
const isSystemInDarkMode = matchMedia("(prefers-color-scheme: dark)")
.matches;
return isSystemInDarkMode ? "dark" : "light";
});
system
mode, we need to track down if they decide to change it from light to dark while still being in our system mode (which is why we are also using a state for the theme
).system
mode, we'll get their current system theme and start an event listener to detect any changes in their theme:React.useEffect(() => {
if (mode !== "system") {
setTheme(mode);
return;
}
const isSystemInDarkMode = matchMedia("(prefers-color-scheme: dark)");
// If system mode, immediately change theme according to the current system value
setTheme(isSystemInDarkMode.matches ? "dark" : "light");
// As the system value can change, we define an event listener when in system mode
// to track down its changes
const listener = (event: MediaQueryListEvent) => {
setTheme(event.matches ? "dark" : "light");
};
isSystemInDarkMode.addListener(listener);
return () => {
isSystemInDarkMode.removeListener(listener);
};
}, [mode]);
theme
state, we can make so that the CSS and the HTML follows this state:React.useEffect(() => {
// Clear previous classNames on the body and add the new one
document.body.classList.remove("light");
document.body.classList.remove("dark");
document.body.classList.add(theme);
// change <meta name="color-scheme"> for native inputs
(document.getElementById("colorScheme") as HTMLMetaElement).content = theme;
}, [theme]);
<ThemeContext.Provider value={{ theme, mode, setMode }}>
{children}
</ThemeContext.Provider>
const { theme, mode, setMode } = React.useContext(ThemeContext);