ThemeProvider.tsx 2.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. import { createContext, useContext, useEffect, useState } from "react";
  2. type Theme = "dark" | "light" | "system";
  3. type ThemeProviderProps = {
  4. children: React.ReactNode;
  5. defaultTheme?: Theme;
  6. storageKey?: string;
  7. };
  8. type ThemeProviderState = {
  9. theme: Theme;
  10. setTheme: (theme: Theme) => void;
  11. };
  12. const initialState: ThemeProviderState = {
  13. theme: "system",
  14. setTheme: () => null,
  15. };
  16. const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
  17. const getThemeFromCookie = (key: string): Theme | null => {
  18. const match = document.cookie.match(new RegExp(`(^| )${key}=([^;]+)`));
  19. if (match && (match[2] === "dark" || match[2] === "light" || match[2] === "system")) {
  20. return match[2] as Theme;
  21. }
  22. return null;
  23. };
  24. export function ThemeProvider({
  25. children,
  26. defaultTheme = "system",
  27. storageKey = "vite-ui-theme",
  28. ...props
  29. }: ThemeProviderProps) {
  30. const [theme, setTheme] = useState<Theme>(() => {
  31. const cookieTheme = getThemeFromCookie(storageKey);
  32. if (cookieTheme) return cookieTheme;
  33. return (localStorage.getItem(storageKey) as Theme) || defaultTheme;
  34. });
  35. useEffect(() => {
  36. const root = window.document.documentElement;
  37. root.classList.remove("light", "dark");
  38. let activeTheme = theme;
  39. if (theme === "system") {
  40. activeTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
  41. }
  42. root.classList.add(activeTheme);
  43. localStorage.setItem(storageKey, theme);
  44. document.cookie = `${storageKey}=${activeTheme}; path=/; max-age=31536000; SameSite=Lax`;
  45. }, [theme, storageKey]);
  46. const value = {
  47. theme,
  48. setTheme: (newTheme: Theme) => {
  49. setTheme(newTheme);
  50. },
  51. };
  52. return (
  53. <ThemeProviderContext.Provider {...props} value={value}>
  54. {children}
  55. </ThemeProviderContext.Provider>
  56. );
  57. }
  58. export const useTheme = () => {
  59. const context = useContext(ThemeProviderContext);
  60. if (context === undefined)
  61. throw new Error("useTheme must be used within a ThemeProvider");
  62. return context;
  63. };