theme-provider.tsx raw
1 import { createContext, useContext, useEffect, useState } from "react";
2
3 export type DarkMode = "system" | "light" | "dark";
4 // eslint-disable-next-line react-refresh/only-export-components
5 export const Themes = [
6 "alby",
7 "classic",
8 "bitcoin",
9 "nostr",
10 "matrix",
11 "ghibli",
12 "claymorphism",
13 ] as const;
14 export type Theme = (typeof Themes)[number];
15
16 type ThemeProviderProps = {
17 children: React.ReactNode;
18 defaultTheme?: Theme;
19 defaultDarkMode?: DarkMode;
20 storageKey?: string;
21 };
22
23 type ThemeProviderState = {
24 theme: string;
25 darkMode: string;
26 setTheme: (theme: Theme) => void;
27 setDarkMode: (mode: DarkMode) => void;
28 isDarkMode: boolean;
29 };
30
31 const initialState: ThemeProviderState = {
32 theme: "alby",
33 setTheme: () => null,
34 darkMode: "system",
35 setDarkMode: () => null,
36 isDarkMode: false,
37 };
38
39 const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
40
41 export function ThemeProvider({
42 children,
43 defaultTheme = "alby",
44 defaultDarkMode = "system",
45 storageKey = "vite-ui-theme",
46 ...props
47 }: ThemeProviderProps) {
48 const [theme, setTheme] = useState<Theme>(() => {
49 const themeFromStorage = localStorage.getItem(storageKey) as Theme;
50 return Themes.includes(themeFromStorage) ? themeFromStorage : defaultTheme;
51 });
52
53 const [darkMode, setDarkMode] = useState<DarkMode>(() => {
54 return (
55 (localStorage.getItem(storageKey + "-darkmode") as DarkMode) ||
56 defaultDarkMode
57 );
58 });
59
60 const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
61
62 useEffect(() => {
63 const root = window.document.documentElement;
64
65 // Find and remove classes that start with 'theme-'
66 const classList = root.classList;
67 classList.forEach((className) => {
68 if (className.startsWith("theme-")) {
69 classList.remove(className);
70 }
71 });
72
73 classList.add(`theme-${theme}`);
74
75 let prefersDark;
76 if (darkMode == "system") {
77 prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
78 } else {
79 prefersDark = darkMode === "dark";
80 }
81
82 setIsDarkMode(prefersDark);
83
84 if (prefersDark) {
85 classList.add("dark");
86 } else {
87 classList.remove("dark");
88 }
89 }, [theme, darkMode]);
90
91 const value = {
92 theme,
93 setTheme: (theme: Theme) => {
94 localStorage.setItem(storageKey, theme);
95 setTheme(theme);
96 },
97 darkMode,
98 setDarkMode: (darkMode: DarkMode) => {
99 localStorage.setItem(storageKey + "-darkmode", darkMode);
100 setDarkMode(darkMode);
101 },
102 isDarkMode,
103 };
104
105 return (
106 <ThemeProviderContext.Provider {...props} value={value}>
107 {children}
108 </ThemeProviderContext.Provider>
109 );
110 }
111
112 // eslint-disable-next-line react-refresh/only-export-components
113 export const useTheme = () => {
114 const context = useContext(ThemeProviderContext);
115
116 if (context === undefined) {
117 throw new Error("useTheme must be used within a ThemeProvider");
118 }
119
120 return context;
121 };
122