Skip to content

Instantly share code, notes, and snippets.

@mihkeleidast
Last active January 20, 2023 05:23
Show Gist options
  • Save mihkeleidast/66ee266c05861f7b181a2e66f2e634f9 to your computer and use it in GitHub Desktop.
Save mihkeleidast/66ee266c05861f7b181a2e66f2e634f9 to your computer and use it in GitHub Desktop.
vanilla-extract themes
import {
lightTheme,
darkTheme,
} from 'component-library';
// Define all themes that can be chosen.
const themes: Record<string, ThemeObject> = {
'light': {
className: lightTheme,
},
'dark': {
className: darkTheme,
},
};
const App = () => {
// Use whatever solution for storing the value, for example save it to a database per user or to localStorage per browser.
const [theme, setTheme] = useStoredTheme();
return (
<ThemeProvider themes={themes} currentTheme={theme} onThemeChange={setTheme}>
{children}
</ThemeProvider>
);
};
import { createTheme } from '@vanilla-extract/css';
import lightVars from './variables-light.json';
import darkVars from './variables-dark.json';
// Create default theme with contract for others
export const [lightTheme, vars] = createTheme(lightVars);
// Create other themes
export const darkTheme = createTheme(vars, darkVars);
import * as React from 'react';
import { useStyle } from './use_style';
import { ThemeCssFile, ThemeObject } from './types';
import { useGlobalClassName } from './use_global_classname';
export interface ThemeProviderProps {
/**
* Your app.
*/
children: React.ReactNode;
/**
* The current theme value. If there is a need to persist the user choice, it needs to be saved externally.
*/
currentTheme: string;
/**
* Map of available themes.
*/
themes: Record<string, ThemeObject>;
/**
* Callback triggered when something calls `setTheme` from the hook.
*/
onThemeChange?: (theme: string) => void;
}
/**
* Provides the current active theme name and variables to its subtree.
*/
export const ThemeProvider = ({ children, currentTheme, themes, onThemeChange }: ThemeProviderProps) => {
const classNameMap = React.useMemo(() => {
return Object.keys(themes).reduce<Record<string, string>>((prev, current) => {
return {
...prev,
[current]: themes[current].className,
};
}, {});
}, [themes]);
useGlobalClassName(currentTheme, classNameMap);
const contextValue = React.useMemo(
() => ({ currentTheme, themes, onThemeChange }),
[currentTheme, themes, onThemeChange],
);
return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>;
};
interface ThemeContext {
currentTheme: string | null;
themes: Record<string, ThemeObject> | null;
onThemeChange?: (theme: string) => void;
}
export const ThemeContext = React.createContext<ThemeContext>({
currentTheme: null,
themes: null,
});
import * as React from 'react';
export const useGlobalClassName = (currentTheme: string, classNameMap: Record<string, string>) => {
React.useEffect(() => {
const currentClassName = classNameMap[currentTheme];
if (!document.documentElement.classList.contains(currentClassName)) {
const allThemeClasses = Object.values(classNameMap);
document.documentElement.classList.remove(...allThemeClasses);
document.documentElement.classList.add(currentClassName);
}
}, [currentTheme, classNameMap]);
};
@dhsrocha
Copy link

dhsrocha commented Oct 3, 2022

Looks gorgeous, but how to deal with ThemeObject?

@mihkeleidast
Copy link
Author

@dhsrocha what do you mean by that question?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment