Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
🌑☀️core app system/light/dark mode theming + varying themes for nested components
import * as React from "react";
type ThemeConfig = "system" | "light" | "dark";
type ThemeName = "light" | "dark";
// Custom themes are keyed by a unique id.
type KeyedThemes = {
[k: string]: {
config: ThemeConfig;
themeName: ThemeName;
};
};
type Props = {
children: React.ReactNode;
};
const APP_THEME_KEY = "appTheme";
const VALID_THEME_CONFIGS: ThemeConfig[] = ["system", "light", "dark"];
const DARK_MODE_MEDIA_QUERY = "(prefers-color-scheme: dark)";
const ThemeContext = React.createContext(undefined);
function getGlobalAppThemeConfig(): ThemeConfig {
if (typeof window !== "undefined") {
const storage = window.localStorage.getItem(APP_THEME_KEY) as ThemeConfig;
const config: ThemeConfig = VALID_THEME_CONFIGS.includes(storage)
? storage
: "system";
return config;
}
return "system";
}
function setGlobalAppThemeConfig(config: ThemeConfig) {
if (config === "system") {
window.localStorage.removeItem(APP_THEME_KEY);
window.document.documentElement.removeAttribute("data-theme");
} else if (config === "dark" || config === "light") {
window.localStorage.setItem(APP_THEME_KEY, config);
window.document.documentElement.setAttribute("data-theme", config);
}
}
export function ThemeProvider(props: Props) {
const [themes, setThemes] = React.useState<KeyedThemes>({});
const [appThemeConfig, setAppThemeConfig] = React.useState(
getGlobalAppThemeConfig
);
const [appThemeName, setAppThemeName] = React.useState<ThemeName | undefined>(
undefined
);
React.useEffect(() => {
const handleThemeChange = (event: MediaQueryListEvent) => {
if (appThemeConfig === "system") {
setAppThemeName(event.matches ? "dark" : "light");
}
};
const mediaQuery = window.matchMedia(DARK_MODE_MEDIA_QUERY);
mediaQuery.addListener(handleThemeChange);
return () => {
mediaQuery.removeListener(handleThemeChange);
};
}, [appThemeConfig]);
React.useEffect(() => {
const theme =
appThemeConfig === "system"
? window.matchMedia(DARK_MODE_MEDIA_QUERY).matches
? "dark"
: "light"
: appThemeConfig;
setAppThemeName(theme);
setGlobalAppThemeConfig(appThemeConfig);
}, [appThemeConfig]);
const setAppConfig = React.useCallback((config: ThemeConfig) => {
if (VALID_THEME_CONFIGS.includes(config)) {
setAppThemeConfig(config);
}
}, []);
const setCustomTheme = React.useCallback(
(key: string, value: ThemeConfig) => {
setThemes({
...themes,
[key]: {
config: value,
themeName: value === "system" ? appThemeName : value,
},
});
},
[appThemeName, themes]
);
const getCustomTheme = React.useCallback(
(key: string) => {
const cached = themes[key];
if (cached) {
return cached;
}
// Default to appTheme.
return {
config: appThemeConfig,
themeName: appThemeName,
};
},
[appThemeConfig, appThemeName, themes]
);
const value = {
setAppConfig,
appThemeConfig,
appThemeName,
getCustomTheme,
setCustomTheme,
};
return (
<ThemeContext.Provider value={value}>
{props.children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return React.useContext(ThemeContext);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.