@philiprehberger/react-theme-provider 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @philiprehberger/react-theme-provider
2
2
 
3
- [![CI](https://github.com/philiprehberger/react-theme-provider/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/react-theme-provider/actions/workflows/ci.yml)
3
+ [![CI](https://github.com/philiprehberger/ts-react-theme-provider/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/ts-react-theme-provider/actions/workflows/ci.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/@philiprehberger/react-theme-provider.svg)](https://www.npmjs.com/package/@philiprehberger/react-theme-provider)
5
- [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/react-theme-provider)](https://github.com/philiprehberger/react-theme-provider/commits/main)
5
+ [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/ts-react-theme-provider)](https://github.com/philiprehberger/ts-react-theme-provider/commits/main)
6
6
 
7
7
  Dark/light/system theme provider for React with localStorage persistence and system preference detection
8
8
 
@@ -44,6 +44,29 @@ function MyComponent() {
44
44
  }
45
45
  ```
46
46
 
47
+ ### useResolvedTheme
48
+
49
+ Shorthand when you only need the resolved value:
50
+
51
+ ```tsx
52
+ import { useResolvedTheme } from '@philiprehberger/react-theme-provider';
53
+
54
+ function Logo() {
55
+ const theme = useResolvedTheme(); // 'light' | 'dark'
56
+ return <img src={theme === 'dark' ? '/logo-dark.svg' : '/logo-light.svg'} />;
57
+ }
58
+ ```
59
+
60
+ ### Disable Transition Flicker
61
+
62
+ ```tsx
63
+ <ThemeProvider disableTransitionOnChange>
64
+ <YourApp />
65
+ </ThemeProvider>
66
+ ```
67
+
68
+ Suppresses CSS transitions for one frame while the new theme class is applied — useful if your app uses long color transitions that would otherwise flicker on switch.
69
+
47
70
  ### ThemeToggle
48
71
 
49
72
  Pre-built toggle component with sun/moon/system icons:
@@ -60,11 +83,12 @@ import { ThemeToggle } from '@philiprehberger/react-theme-provider';
60
83
  |--------|------|-------------|
61
84
  | `ThemeProvider` | Component | Context provider with localStorage persistence and system preference detection |
62
85
  | `useTheme()` | Hook | Returns `{ theme, setTheme, resolvedTheme }` for reading/setting theme |
86
+ | `useResolvedTheme()` | Hook | Returns just the resolved theme: `'light' \| 'dark'` |
63
87
  | `ThemeToggle` | Component | Pre-built three-way toggle (light/dark/system) with sun/moon/system icons |
64
88
  | `Theme` | Type | `'light' \| 'dark' \| 'system'` |
65
89
  | `ResolvedTheme` | Type | `'light' \| 'dark'` (the actual applied theme) |
66
90
  | `ThemeContextType` | Type | Shape of the theme context value |
67
- | `ThemeProviderProps` | Type | Props for `ThemeProvider` (`children`, `storageKey?`, `defaultTheme?`) |
91
+ | `ThemeProviderProps` | Type | Props for `ThemeProvider` (`children`, `storageKey?`, `defaultTheme?`, `disableTransitionOnChange?`) |
68
92
  | `ThemeToggleProps` | Type | Props for `ThemeToggle` (`className?`, `activeClassName?`, `inactiveClassName?`) |
69
93
 
70
94
  ## How It Works
@@ -86,11 +110,11 @@ npm test
86
110
 
87
111
  If you find this project useful:
88
112
 
89
- ⭐ [Star the repo](https://github.com/philiprehberger/react-theme-provider)
113
+ ⭐ [Star the repo](https://github.com/philiprehberger/ts-react-theme-provider)
90
114
 
91
- 🐛 [Report issues](https://github.com/philiprehberger/react-theme-provider/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
115
+ 🐛 [Report issues](https://github.com/philiprehberger/ts-react-theme-provider/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
92
116
 
93
- 💡 [Suggest features](https://github.com/philiprehberger/react-theme-provider/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
117
+ 💡 [Suggest features](https://github.com/philiprehberger/ts-react-theme-provider/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
94
118
 
95
119
  ❤️ [Sponsor development](https://github.com/sponsors/philiprehberger)
96
120
 
package/dist/index.cjs CHANGED
@@ -4,10 +4,26 @@ var react = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
 
6
6
  var ThemeContext = react.createContext(void 0);
7
+ function suppressTransitions() {
8
+ const css = document.createElement("style");
9
+ css.appendChild(
10
+ document.createTextNode(
11
+ `*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`
12
+ )
13
+ );
14
+ document.head.appendChild(css);
15
+ void window.getComputedStyle(document.body).opacity;
16
+ requestAnimationFrame(() => {
17
+ requestAnimationFrame(() => {
18
+ document.head.removeChild(css);
19
+ });
20
+ });
21
+ }
7
22
  function ThemeProvider({
8
23
  children,
9
24
  storageKey = "theme",
10
- defaultTheme = "system"
25
+ defaultTheme = "system",
26
+ disableTransitionOnChange = false
11
27
  }) {
12
28
  const [theme, setTheme] = react.useState(defaultTheme);
13
29
  const [resolvedTheme, setResolvedTheme] = react.useState("light");
@@ -29,11 +45,14 @@ function ThemeProvider({
29
45
  } else {
30
46
  resolved = theme;
31
47
  }
48
+ if (disableTransitionOnChange) {
49
+ suppressTransitions();
50
+ }
32
51
  setResolvedTheme(resolved);
33
52
  root.classList.remove("light", "dark");
34
53
  root.classList.add(resolved);
35
54
  localStorage.setItem(storageKey, theme);
36
- }, [theme, mounted, storageKey]);
55
+ }, [theme, mounted, storageKey, disableTransitionOnChange]);
37
56
  react.useEffect(() => {
38
57
  if (!mounted || theme !== "system") return;
39
58
  const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
@@ -54,6 +73,9 @@ function useTheme() {
54
73
  }
55
74
  return context;
56
75
  }
76
+ function useResolvedTheme() {
77
+ return useTheme().resolvedTheme;
78
+ }
57
79
  var SunIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "h-5 w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" }) });
58
80
  var MoonIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "h-5 w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" }) });
59
81
  var SystemIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "h-5 w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) });
@@ -84,6 +106,7 @@ function ThemeToggle({
84
106
 
85
107
  exports.ThemeProvider = ThemeProvider;
86
108
  exports.ThemeToggle = ThemeToggle;
109
+ exports.useResolvedTheme = useResolvedTheme;
87
110
  exports.useTheme = useTheme;
88
111
  //# sourceMappingURL=index.cjs.map
89
112
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ThemeProvider.tsx","../src/ThemeToggle.tsx"],"names":["createContext","useState","useEffect","jsx","useContext"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\n\nexport type Theme = 'light' | 'dark' | 'system';\nexport type ResolvedTheme = 'light' | 'dark';\n\nexport interface ThemeContextType {\n theme: Theme;\n setTheme: (theme: Theme) => void;\n resolvedTheme: ResolvedTheme;\n}\n\nconst ThemeContext = createContext<ThemeContextType | undefined>(undefined);\n\nexport interface ThemeProviderProps {\n children: ReactNode;\n /** localStorage key for persisting theme (default: \"theme\") */\n storageKey?: string;\n /** Default theme when no stored preference exists (default: \"system\") */\n defaultTheme?: Theme;\n}\n\n/**\n * Theme provider with dark/light/system support.\n * Persists preference to localStorage and syncs with system preference changes.\n * Applies theme by toggling \"light\"/\"dark\" classes on the document element.\n */\nexport function ThemeProvider({\n children,\n storageKey = 'theme',\n defaultTheme = 'system',\n}: ThemeProviderProps) {\n const [theme, setTheme] = useState<Theme>(defaultTheme);\n const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>('light');\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n const stored = localStorage.getItem(storageKey) as Theme | null;\n if (stored && ['light', 'dark', 'system'].includes(stored)) {\n setTheme(stored);\n }\n }, [storageKey]);\n\n useEffect(() => {\n if (!mounted) return;\n\n const root = document.documentElement;\n let resolved: ResolvedTheme;\n\n if (theme === 'system') {\n const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n resolved = systemDark ? 'dark' : 'light';\n } else {\n resolved = theme;\n }\n\n setResolvedTheme(resolved);\n root.classList.remove('light', 'dark');\n root.classList.add(resolved);\n localStorage.setItem(storageKey, theme);\n }, [theme, mounted, storageKey]);\n\n useEffect(() => {\n if (!mounted || theme !== 'system') return;\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n const handleChange = (e: MediaQueryListEvent) => {\n setResolvedTheme(e.matches ? 'dark' : 'light');\n document.documentElement.classList.remove('light', 'dark');\n document.documentElement.classList.add(e.matches ? 'dark' : 'light');\n };\n\n mediaQuery.addEventListener('change', handleChange);\n return () => mediaQuery.removeEventListener('change', handleChange);\n }, [theme, mounted]);\n\n return (\n <ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>\n {children}\n </ThemeContext.Provider>\n );\n}\n\n/**\n * Access the current theme context.\n * Must be used within a ThemeProvider.\n */\nexport function useTheme(): ThemeContextType {\n const context = useContext(ThemeContext);\n if (!context) {\n throw new Error('useTheme must be used within a ThemeProvider');\n }\n return context;\n}\n","import { type ReactNode } from 'react';\nimport { useTheme, type Theme } from './ThemeProvider';\n\nconst SunIcon = () => (\n <svg className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z\" />\n </svg>\n);\n\nconst MoonIcon = () => (\n <svg className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z\" />\n </svg>\n);\n\nconst SystemIcon = () => (\n <svg className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\" />\n </svg>\n);\n\ninterface ThemeOption {\n value: Theme;\n icon: () => ReactNode;\n label: string;\n}\n\nexport interface ThemeToggleProps {\n /** CSS class for the outer container */\n className?: string;\n /** CSS class for the active button */\n activeClassName?: string;\n /** CSS class for inactive buttons */\n inactiveClassName?: string;\n}\n\n/**\n * Three-way theme toggle (light / dark / system).\n * Uses radio group semantics for accessibility.\n */\nexport function ThemeToggle({\n className = 'flex items-center gap-1 rounded-lg bg-gray-100 p-1 dark:bg-gray-800',\n activeClassName = 'rounded-md p-2 transition-colors bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white',\n inactiveClassName = 'rounded-md p-2 transition-colors text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white',\n}: ThemeToggleProps) {\n const { theme, setTheme } = useTheme();\n\n const options: ThemeOption[] = [\n { value: 'light', icon: SunIcon, label: 'Light' },\n { value: 'dark', icon: MoonIcon, label: 'Dark' },\n { value: 'system', icon: SystemIcon, label: 'System' },\n ];\n\n return (\n <div className={className} role=\"radiogroup\" aria-label=\"Theme selection\">\n {options.map(({ value, icon: Icon, label }) => (\n <button\n key={value}\n onClick={() => setTheme(value)}\n role=\"radio\"\n aria-checked={theme === value}\n aria-label={`${label} theme`}\n className={theme === value ? activeClassName : inactiveClassName}\n >\n <Icon />\n </button>\n ))}\n </div>\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/ThemeProvider.tsx","../src/ThemeToggle.tsx"],"names":["createContext","useState","useEffect","jsx","useContext"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\n\nexport type Theme = 'light' | 'dark' | 'system';\nexport type ResolvedTheme = 'light' | 'dark';\n\nexport interface ThemeContextType {\n theme: Theme;\n setTheme: (theme: Theme) => void;\n resolvedTheme: ResolvedTheme;\n}\n\nconst ThemeContext = createContext<ThemeContextType | undefined>(undefined);\n\nexport interface ThemeProviderProps {\n children: ReactNode;\n /** localStorage key for persisting theme (default: \"theme\") */\n storageKey?: string;\n /** Default theme when no stored preference exists (default: \"system\") */\n defaultTheme?: Theme;\n /**\n * When true, suppresses CSS transitions for one frame during theme change to\n * avoid color-flicker on slow paint paths. Default: false.\n */\n disableTransitionOnChange?: boolean;\n}\n\nfunction suppressTransitions(): void {\n const css = document.createElement('style');\n css.appendChild(\n document.createTextNode(\n `*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`,\n ),\n );\n document.head.appendChild(css);\n // Force reflow then remove on next frame\n void window.getComputedStyle(document.body).opacity;\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n document.head.removeChild(css);\n });\n });\n}\n\n/**\n * Theme provider with dark/light/system support.\n * Persists preference to localStorage and syncs with system preference changes.\n * Applies theme by toggling \"light\"/\"dark\" classes on the document element.\n */\nexport function ThemeProvider({\n children,\n storageKey = 'theme',\n defaultTheme = 'system',\n disableTransitionOnChange = false,\n}: ThemeProviderProps) {\n const [theme, setTheme] = useState<Theme>(defaultTheme);\n const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>('light');\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n const stored = localStorage.getItem(storageKey) as Theme | null;\n if (stored && ['light', 'dark', 'system'].includes(stored)) {\n setTheme(stored);\n }\n }, [storageKey]);\n\n useEffect(() => {\n if (!mounted) return;\n\n const root = document.documentElement;\n let resolved: ResolvedTheme;\n\n if (theme === 'system') {\n const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n resolved = systemDark ? 'dark' : 'light';\n } else {\n resolved = theme;\n }\n\n if (disableTransitionOnChange) {\n suppressTransitions();\n }\n\n setResolvedTheme(resolved);\n root.classList.remove('light', 'dark');\n root.classList.add(resolved);\n localStorage.setItem(storageKey, theme);\n }, [theme, mounted, storageKey, disableTransitionOnChange]);\n\n useEffect(() => {\n if (!mounted || theme !== 'system') return;\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n const handleChange = (e: MediaQueryListEvent) => {\n setResolvedTheme(e.matches ? 'dark' : 'light');\n document.documentElement.classList.remove('light', 'dark');\n document.documentElement.classList.add(e.matches ? 'dark' : 'light');\n };\n\n mediaQuery.addEventListener('change', handleChange);\n return () => mediaQuery.removeEventListener('change', handleChange);\n }, [theme, mounted]);\n\n return (\n <ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>\n {children}\n </ThemeContext.Provider>\n );\n}\n\n/**\n * Access the current theme context.\n * Must be used within a ThemeProvider.\n */\nexport function useTheme(): ThemeContextType {\n const context = useContext(ThemeContext);\n if (!context) {\n throw new Error('useTheme must be used within a ThemeProvider');\n }\n return context;\n}\n\n/**\n * Shorthand hook returning just the resolved theme ('light' or 'dark').\n */\nexport function useResolvedTheme(): ResolvedTheme {\n return useTheme().resolvedTheme;\n}\n","import { type ReactNode } from 'react';\nimport { useTheme, type Theme } from './ThemeProvider';\n\nconst SunIcon = () => (\n <svg className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z\" />\n </svg>\n);\n\nconst MoonIcon = () => (\n <svg className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z\" />\n </svg>\n);\n\nconst SystemIcon = () => (\n <svg className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\" />\n </svg>\n);\n\ninterface ThemeOption {\n value: Theme;\n icon: () => ReactNode;\n label: string;\n}\n\nexport interface ThemeToggleProps {\n /** CSS class for the outer container */\n className?: string;\n /** CSS class for the active button */\n activeClassName?: string;\n /** CSS class for inactive buttons */\n inactiveClassName?: string;\n}\n\n/**\n * Three-way theme toggle (light / dark / system).\n * Uses radio group semantics for accessibility.\n */\nexport function ThemeToggle({\n className = 'flex items-center gap-1 rounded-lg bg-gray-100 p-1 dark:bg-gray-800',\n activeClassName = 'rounded-md p-2 transition-colors bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white',\n inactiveClassName = 'rounded-md p-2 transition-colors text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white',\n}: ThemeToggleProps) {\n const { theme, setTheme } = useTheme();\n\n const options: ThemeOption[] = [\n { value: 'light', icon: SunIcon, label: 'Light' },\n { value: 'dark', icon: MoonIcon, label: 'Dark' },\n { value: 'system', icon: SystemIcon, label: 'System' },\n ];\n\n return (\n <div className={className} role=\"radiogroup\" aria-label=\"Theme selection\">\n {options.map(({ value, icon: Icon, label }) => (\n <button\n key={value}\n onClick={() => setTheme(value)}\n role=\"radio\"\n aria-checked={theme === value}\n aria-label={`${label} theme`}\n className={theme === value ? activeClassName : inactiveClassName}\n >\n <Icon />\n </button>\n ))}\n </div>\n );\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -14,18 +14,27 @@ interface ThemeProviderProps {
14
14
  storageKey?: string;
15
15
  /** Default theme when no stored preference exists (default: "system") */
16
16
  defaultTheme?: Theme;
17
+ /**
18
+ * When true, suppresses CSS transitions for one frame during theme change to
19
+ * avoid color-flicker on slow paint paths. Default: false.
20
+ */
21
+ disableTransitionOnChange?: boolean;
17
22
  }
18
23
  /**
19
24
  * Theme provider with dark/light/system support.
20
25
  * Persists preference to localStorage and syncs with system preference changes.
21
26
  * Applies theme by toggling "light"/"dark" classes on the document element.
22
27
  */
23
- declare function ThemeProvider({ children, storageKey, defaultTheme, }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
28
+ declare function ThemeProvider({ children, storageKey, defaultTheme, disableTransitionOnChange, }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
24
29
  /**
25
30
  * Access the current theme context.
26
31
  * Must be used within a ThemeProvider.
27
32
  */
28
33
  declare function useTheme(): ThemeContextType;
34
+ /**
35
+ * Shorthand hook returning just the resolved theme ('light' or 'dark').
36
+ */
37
+ declare function useResolvedTheme(): ResolvedTheme;
29
38
 
30
39
  interface ThemeToggleProps {
31
40
  /** CSS class for the outer container */
@@ -41,4 +50,4 @@ interface ThemeToggleProps {
41
50
  */
42
51
  declare function ThemeToggle({ className, activeClassName, inactiveClassName, }: ThemeToggleProps): react_jsx_runtime.JSX.Element;
43
52
 
44
- export { type ResolvedTheme, type Theme, type ThemeContextType, ThemeProvider, type ThemeProviderProps, ThemeToggle, type ThemeToggleProps, useTheme };
53
+ export { type ResolvedTheme, type Theme, type ThemeContextType, ThemeProvider, type ThemeProviderProps, ThemeToggle, type ThemeToggleProps, useResolvedTheme, useTheme };
package/dist/index.d.ts CHANGED
@@ -14,18 +14,27 @@ interface ThemeProviderProps {
14
14
  storageKey?: string;
15
15
  /** Default theme when no stored preference exists (default: "system") */
16
16
  defaultTheme?: Theme;
17
+ /**
18
+ * When true, suppresses CSS transitions for one frame during theme change to
19
+ * avoid color-flicker on slow paint paths. Default: false.
20
+ */
21
+ disableTransitionOnChange?: boolean;
17
22
  }
18
23
  /**
19
24
  * Theme provider with dark/light/system support.
20
25
  * Persists preference to localStorage and syncs with system preference changes.
21
26
  * Applies theme by toggling "light"/"dark" classes on the document element.
22
27
  */
23
- declare function ThemeProvider({ children, storageKey, defaultTheme, }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
28
+ declare function ThemeProvider({ children, storageKey, defaultTheme, disableTransitionOnChange, }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
24
29
  /**
25
30
  * Access the current theme context.
26
31
  * Must be used within a ThemeProvider.
27
32
  */
28
33
  declare function useTheme(): ThemeContextType;
34
+ /**
35
+ * Shorthand hook returning just the resolved theme ('light' or 'dark').
36
+ */
37
+ declare function useResolvedTheme(): ResolvedTheme;
29
38
 
30
39
  interface ThemeToggleProps {
31
40
  /** CSS class for the outer container */
@@ -41,4 +50,4 @@ interface ThemeToggleProps {
41
50
  */
42
51
  declare function ThemeToggle({ className, activeClassName, inactiveClassName, }: ThemeToggleProps): react_jsx_runtime.JSX.Element;
43
52
 
44
- export { type ResolvedTheme, type Theme, type ThemeContextType, ThemeProvider, type ThemeProviderProps, ThemeToggle, type ThemeToggleProps, useTheme };
53
+ export { type ResolvedTheme, type Theme, type ThemeContextType, ThemeProvider, type ThemeProviderProps, ThemeToggle, type ThemeToggleProps, useResolvedTheme, useTheme };
package/dist/index.js CHANGED
@@ -2,10 +2,26 @@ import { createContext, useState, useEffect, useContext } from 'react';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
 
4
4
  var ThemeContext = createContext(void 0);
5
+ function suppressTransitions() {
6
+ const css = document.createElement("style");
7
+ css.appendChild(
8
+ document.createTextNode(
9
+ `*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`
10
+ )
11
+ );
12
+ document.head.appendChild(css);
13
+ void window.getComputedStyle(document.body).opacity;
14
+ requestAnimationFrame(() => {
15
+ requestAnimationFrame(() => {
16
+ document.head.removeChild(css);
17
+ });
18
+ });
19
+ }
5
20
  function ThemeProvider({
6
21
  children,
7
22
  storageKey = "theme",
8
- defaultTheme = "system"
23
+ defaultTheme = "system",
24
+ disableTransitionOnChange = false
9
25
  }) {
10
26
  const [theme, setTheme] = useState(defaultTheme);
11
27
  const [resolvedTheme, setResolvedTheme] = useState("light");
@@ -27,11 +43,14 @@ function ThemeProvider({
27
43
  } else {
28
44
  resolved = theme;
29
45
  }
46
+ if (disableTransitionOnChange) {
47
+ suppressTransitions();
48
+ }
30
49
  setResolvedTheme(resolved);
31
50
  root.classList.remove("light", "dark");
32
51
  root.classList.add(resolved);
33
52
  localStorage.setItem(storageKey, theme);
34
- }, [theme, mounted, storageKey]);
53
+ }, [theme, mounted, storageKey, disableTransitionOnChange]);
35
54
  useEffect(() => {
36
55
  if (!mounted || theme !== "system") return;
37
56
  const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
@@ -52,6 +71,9 @@ function useTheme() {
52
71
  }
53
72
  return context;
54
73
  }
74
+ function useResolvedTheme() {
75
+ return useTheme().resolvedTheme;
76
+ }
55
77
  var SunIcon = () => /* @__PURE__ */ jsx("svg", { className: "h-5 w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" }) });
56
78
  var MoonIcon = () => /* @__PURE__ */ jsx("svg", { className: "h-5 w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" }) });
57
79
  var SystemIcon = () => /* @__PURE__ */ jsx("svg", { className: "h-5 w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) });
@@ -80,6 +102,6 @@ function ThemeToggle({
80
102
  )) });
81
103
  }
82
104
 
83
- export { ThemeProvider, ThemeToggle, useTheme };
105
+ export { ThemeProvider, ThemeToggle, useResolvedTheme, useTheme };
84
106
  //# sourceMappingURL=index.js.map
85
107
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ThemeProvider.tsx","../src/ThemeToggle.tsx"],"names":["jsx"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\n\nexport type Theme = 'light' | 'dark' | 'system';\nexport type ResolvedTheme = 'light' | 'dark';\n\nexport interface ThemeContextType {\n theme: Theme;\n setTheme: (theme: Theme) => void;\n resolvedTheme: ResolvedTheme;\n}\n\nconst ThemeContext = createContext<ThemeContextType | undefined>(undefined);\n\nexport interface ThemeProviderProps {\n children: ReactNode;\n /** localStorage key for persisting theme (default: \"theme\") */\n storageKey?: string;\n /** Default theme when no stored preference exists (default: \"system\") */\n defaultTheme?: Theme;\n}\n\n/**\n * Theme provider with dark/light/system support.\n * Persists preference to localStorage and syncs with system preference changes.\n * Applies theme by toggling \"light\"/\"dark\" classes on the document element.\n */\nexport function ThemeProvider({\n children,\n storageKey = 'theme',\n defaultTheme = 'system',\n}: ThemeProviderProps) {\n const [theme, setTheme] = useState<Theme>(defaultTheme);\n const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>('light');\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n const stored = localStorage.getItem(storageKey) as Theme | null;\n if (stored && ['light', 'dark', 'system'].includes(stored)) {\n setTheme(stored);\n }\n }, [storageKey]);\n\n useEffect(() => {\n if (!mounted) return;\n\n const root = document.documentElement;\n let resolved: ResolvedTheme;\n\n if (theme === 'system') {\n const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n resolved = systemDark ? 'dark' : 'light';\n } else {\n resolved = theme;\n }\n\n setResolvedTheme(resolved);\n root.classList.remove('light', 'dark');\n root.classList.add(resolved);\n localStorage.setItem(storageKey, theme);\n }, [theme, mounted, storageKey]);\n\n useEffect(() => {\n if (!mounted || theme !== 'system') return;\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n const handleChange = (e: MediaQueryListEvent) => {\n setResolvedTheme(e.matches ? 'dark' : 'light');\n document.documentElement.classList.remove('light', 'dark');\n document.documentElement.classList.add(e.matches ? 'dark' : 'light');\n };\n\n mediaQuery.addEventListener('change', handleChange);\n return () => mediaQuery.removeEventListener('change', handleChange);\n }, [theme, mounted]);\n\n return (\n <ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>\n {children}\n </ThemeContext.Provider>\n );\n}\n\n/**\n * Access the current theme context.\n * Must be used within a ThemeProvider.\n */\nexport function useTheme(): ThemeContextType {\n const context = useContext(ThemeContext);\n if (!context) {\n throw new Error('useTheme must be used within a ThemeProvider');\n }\n return context;\n}\n","import { type ReactNode } from 'react';\nimport { useTheme, type Theme } from './ThemeProvider';\n\nconst SunIcon = () => (\n <svg className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z\" />\n </svg>\n);\n\nconst MoonIcon = () => (\n <svg className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z\" />\n </svg>\n);\n\nconst SystemIcon = () => (\n <svg className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\" />\n </svg>\n);\n\ninterface ThemeOption {\n value: Theme;\n icon: () => ReactNode;\n label: string;\n}\n\nexport interface ThemeToggleProps {\n /** CSS class for the outer container */\n className?: string;\n /** CSS class for the active button */\n activeClassName?: string;\n /** CSS class for inactive buttons */\n inactiveClassName?: string;\n}\n\n/**\n * Three-way theme toggle (light / dark / system).\n * Uses radio group semantics for accessibility.\n */\nexport function ThemeToggle({\n className = 'flex items-center gap-1 rounded-lg bg-gray-100 p-1 dark:bg-gray-800',\n activeClassName = 'rounded-md p-2 transition-colors bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white',\n inactiveClassName = 'rounded-md p-2 transition-colors text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white',\n}: ThemeToggleProps) {\n const { theme, setTheme } = useTheme();\n\n const options: ThemeOption[] = [\n { value: 'light', icon: SunIcon, label: 'Light' },\n { value: 'dark', icon: MoonIcon, label: 'Dark' },\n { value: 'system', icon: SystemIcon, label: 'System' },\n ];\n\n return (\n <div className={className} role=\"radiogroup\" aria-label=\"Theme selection\">\n {options.map(({ value, icon: Icon, label }) => (\n <button\n key={value}\n onClick={() => setTheme(value)}\n role=\"radio\"\n aria-checked={theme === value}\n aria-label={`${label} theme`}\n className={theme === value ? activeClassName : inactiveClassName}\n >\n <Icon />\n </button>\n ))}\n </div>\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/ThemeProvider.tsx","../src/ThemeToggle.tsx"],"names":["jsx"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\n\nexport type Theme = 'light' | 'dark' | 'system';\nexport type ResolvedTheme = 'light' | 'dark';\n\nexport interface ThemeContextType {\n theme: Theme;\n setTheme: (theme: Theme) => void;\n resolvedTheme: ResolvedTheme;\n}\n\nconst ThemeContext = createContext<ThemeContextType | undefined>(undefined);\n\nexport interface ThemeProviderProps {\n children: ReactNode;\n /** localStorage key for persisting theme (default: \"theme\") */\n storageKey?: string;\n /** Default theme when no stored preference exists (default: \"system\") */\n defaultTheme?: Theme;\n /**\n * When true, suppresses CSS transitions for one frame during theme change to\n * avoid color-flicker on slow paint paths. Default: false.\n */\n disableTransitionOnChange?: boolean;\n}\n\nfunction suppressTransitions(): void {\n const css = document.createElement('style');\n css.appendChild(\n document.createTextNode(\n `*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`,\n ),\n );\n document.head.appendChild(css);\n // Force reflow then remove on next frame\n void window.getComputedStyle(document.body).opacity;\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n document.head.removeChild(css);\n });\n });\n}\n\n/**\n * Theme provider with dark/light/system support.\n * Persists preference to localStorage and syncs with system preference changes.\n * Applies theme by toggling \"light\"/\"dark\" classes on the document element.\n */\nexport function ThemeProvider({\n children,\n storageKey = 'theme',\n defaultTheme = 'system',\n disableTransitionOnChange = false,\n}: ThemeProviderProps) {\n const [theme, setTheme] = useState<Theme>(defaultTheme);\n const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>('light');\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n const stored = localStorage.getItem(storageKey) as Theme | null;\n if (stored && ['light', 'dark', 'system'].includes(stored)) {\n setTheme(stored);\n }\n }, [storageKey]);\n\n useEffect(() => {\n if (!mounted) return;\n\n const root = document.documentElement;\n let resolved: ResolvedTheme;\n\n if (theme === 'system') {\n const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n resolved = systemDark ? 'dark' : 'light';\n } else {\n resolved = theme;\n }\n\n if (disableTransitionOnChange) {\n suppressTransitions();\n }\n\n setResolvedTheme(resolved);\n root.classList.remove('light', 'dark');\n root.classList.add(resolved);\n localStorage.setItem(storageKey, theme);\n }, [theme, mounted, storageKey, disableTransitionOnChange]);\n\n useEffect(() => {\n if (!mounted || theme !== 'system') return;\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n const handleChange = (e: MediaQueryListEvent) => {\n setResolvedTheme(e.matches ? 'dark' : 'light');\n document.documentElement.classList.remove('light', 'dark');\n document.documentElement.classList.add(e.matches ? 'dark' : 'light');\n };\n\n mediaQuery.addEventListener('change', handleChange);\n return () => mediaQuery.removeEventListener('change', handleChange);\n }, [theme, mounted]);\n\n return (\n <ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>\n {children}\n </ThemeContext.Provider>\n );\n}\n\n/**\n * Access the current theme context.\n * Must be used within a ThemeProvider.\n */\nexport function useTheme(): ThemeContextType {\n const context = useContext(ThemeContext);\n if (!context) {\n throw new Error('useTheme must be used within a ThemeProvider');\n }\n return context;\n}\n\n/**\n * Shorthand hook returning just the resolved theme ('light' or 'dark').\n */\nexport function useResolvedTheme(): ResolvedTheme {\n return useTheme().resolvedTheme;\n}\n","import { type ReactNode } from 'react';\nimport { useTheme, type Theme } from './ThemeProvider';\n\nconst SunIcon = () => (\n <svg className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z\" />\n </svg>\n);\n\nconst MoonIcon = () => (\n <svg className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z\" />\n </svg>\n);\n\nconst SystemIcon = () => (\n <svg className=\"h-5 w-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\" />\n </svg>\n);\n\ninterface ThemeOption {\n value: Theme;\n icon: () => ReactNode;\n label: string;\n}\n\nexport interface ThemeToggleProps {\n /** CSS class for the outer container */\n className?: string;\n /** CSS class for the active button */\n activeClassName?: string;\n /** CSS class for inactive buttons */\n inactiveClassName?: string;\n}\n\n/**\n * Three-way theme toggle (light / dark / system).\n * Uses radio group semantics for accessibility.\n */\nexport function ThemeToggle({\n className = 'flex items-center gap-1 rounded-lg bg-gray-100 p-1 dark:bg-gray-800',\n activeClassName = 'rounded-md p-2 transition-colors bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white',\n inactiveClassName = 'rounded-md p-2 transition-colors text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white',\n}: ThemeToggleProps) {\n const { theme, setTheme } = useTheme();\n\n const options: ThemeOption[] = [\n { value: 'light', icon: SunIcon, label: 'Light' },\n { value: 'dark', icon: MoonIcon, label: 'Dark' },\n { value: 'system', icon: SystemIcon, label: 'System' },\n ];\n\n return (\n <div className={className} role=\"radiogroup\" aria-label=\"Theme selection\">\n {options.map(({ value, icon: Icon, label }) => (\n <button\n key={value}\n onClick={() => setTheme(value)}\n role=\"radio\"\n aria-checked={theme === value}\n aria-label={`${label} theme`}\n className={theme === value ? activeClassName : inactiveClassName}\n >\n <Icon />\n </button>\n ))}\n </div>\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@philiprehberger/react-theme-provider",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "Dark/light/system theme provider for React with localStorage persistence and system preference detection",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -35,7 +35,7 @@
35
35
  "@types/react": "^19.0.0",
36
36
  "react": "^19.0.0",
37
37
  "tsup": "^8.0.0",
38
- "typescript": "^5.0.0"
38
+ "typescript": "^6.0.2"
39
39
  },
40
40
  "keywords": [
41
41
  "react",