ssr-themes 0.0.1-alpha.1

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/LICENSE.md ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,607 @@
1
+ # ssr-themes ![ssr-themes minzip package size](https://img.shields.io/bundlephobia/minzip/ssr-themes) [![Version](https://img.shields.io/npm/v/ssr-themes.svg?colorB=green)](https://www.npmjs.com/package/ssr-themes)
2
+
3
+ Themes for your React SSR app.
4
+
5
+ - ✅ Perfect dark mode with no flashing
6
+ - ✅ System setting with `prefers-color-scheme`
7
+ - ✅ Themed browser UI with color-scheme
8
+ - ✅ SSR support for all frameworks (including TanStack Start/Next)
9
+ - ✅ Easy Tailwind integration
10
+ - ✅ Sync theme across tabs
11
+ - ✅ `useTheme` hook
12
+
13
+ Check out the live examples:
14
+
15
+ - Tanstack Start: []
16
+ - Next: []
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install ssr-themes
22
+ # or
23
+ bun add ssr-themes
24
+ # or
25
+ pnpm install ssr-themes
26
+ # or
27
+ yarn add ssr-themes
28
+ ```
29
+
30
+ ## Use
31
+
32
+ ### With TanStack Start
33
+
34
+ Add `ThemeProvider` to your root route and render `HeadContent`/`Scripts` in the document:
35
+
36
+ ```tsx
37
+ // src/routes/__root.tsx
38
+ import {
39
+ HeadContent,
40
+ Outlet,
41
+ Scripts,
42
+ createRootRoute,
43
+ } from '@tanstack/react-router';
44
+ import type {ReactNode} from 'react';
45
+ import {ThemeProvider} from 'ssr-themes';
46
+ import appCss from '../styles.css?url';
47
+
48
+ function RootDocument({children}: {children: ReactNode}) {
49
+ return (
50
+ <html lang="en" suppressHydrationWarning>
51
+ <head>
52
+ <HeadContent />
53
+ </head>
54
+ <body>
55
+ <ThemeProvider attribute="class">{children}</ThemeProvider>
56
+ <Scripts />
57
+ </body>
58
+ </html>
59
+ );
60
+ }
61
+
62
+ export const Route = createRootRoute({
63
+ head: () => ({
64
+ links: [{rel: 'stylesheet', href: appCss}],
65
+ }),
66
+ component: function RootComponent() {
67
+ return (
68
+ <RootDocument>
69
+ <Outlet />
70
+ </RootDocument>
71
+ );
72
+ },
73
+ });
74
+ ```
75
+
76
+ > **Note!** If you do not add [suppressHydrationWarning](https://reactjs.org/docs/dom-elements.html#suppresshydrationwarning:~:text=It%20only%20works%20one%20level%20deep) to your `<html>` you will get warnings because `ssr-themes` updates that element. This property only applies one level deep, so it won't block hydration warnings on other elements.
77
+
78
+ ### SSR / RSC
79
+
80
+ `ssr-themes` uses cookies to set the theme, so the server can use `registerTheme` to read cookies and apply the right value directly to `<html>`. The inline script will reuse that value and skip cookie reads when the theme attribute/class is already present on the `<html>` element.
81
+
82
+ ```tsx
83
+ import {registerTheme, ThemeProvider} from 'ssr-themes';
84
+ import {cookies} from 'next/headers';
85
+
86
+ const cookieStore = await cookies();
87
+ const theme = cookieStore.get('theme'); // 'dark' | 'light' | undefined
88
+ const htmlProps = registerTheme({
89
+ theme,
90
+ attribute: 'class',
91
+ });
92
+
93
+ return (
94
+ <html suppressHydrationWarning {...htmlProps}>
95
+ <body>
96
+ <ThemeProvider attribute="class">{children}</ThemeProvider>
97
+ </body>
98
+ </html>
99
+ );
100
+ ```
101
+
102
+ ### HTML & CSS
103
+
104
+ That's it, your app fully supports dark mode, including System preference with `prefers-color-scheme`. The theme is also immediately synced between tabs. By default, ssr-themes modifies the `data-theme` attribute on the `html` element, which you can easily use to style your app:
105
+
106
+ ```css
107
+ :root {
108
+ /* Your default theme */
109
+ --background: white;
110
+ --foreground: black;
111
+ }
112
+
113
+ [data-theme='dark'] {
114
+ --background: black;
115
+ --foreground: white;
116
+ }
117
+ ```
118
+
119
+ > **Note!** If you set the attribute of your Theme Provider to class for Tailwind ssr-themes will modify the `class` attribute on the `html` element. See [With TailwindCSS](#with-tailwindcss).
120
+
121
+ ### useTheme
122
+
123
+ Your UI will need to know the current theme and be able to change it. The `useTheme` hook provides theme information:
124
+
125
+ ```jsx
126
+ import {useTheme} from 'ssr-themes';
127
+
128
+ const ThemeChanger = () => {
129
+ const {theme, setTheme} = useTheme();
130
+
131
+ return (
132
+ <div>
133
+ The current theme is: {theme}
134
+ <button onClick={() => setTheme('light')}>Light Mode</button>
135
+ <button onClick={() => setTheme('dark')}>Dark Mode</button>
136
+ </div>
137
+ );
138
+ };
139
+ ```
140
+
141
+ > **Warning!** The above code is hydration _unsafe_ and will throw a hydration mismatch warning when rendering with SSG or SSR. This is because we cannot know the `theme` on the server, so it will always be `undefined` until mounted on the client.
142
+ >
143
+ > You should delay rendering any theme toggling UI until mounted on the client. See the [example](#avoid-hydration-mismatch).
144
+
145
+ ## API
146
+
147
+ Let's dig into the details.
148
+
149
+ ### ThemeProvider
150
+
151
+ All your theme configuration is passed to ThemeProvider.
152
+
153
+ - `cookie = { name: 'theme', path: '/', maxAge: 31536000, sameSite: 'lax' }`: Cookie configuration for the theme cookie
154
+ - `defaultTheme = 'system'`: Default theme name (for v0.0.12 and lower the default was `light`). If `enableSystem` is false, the default theme is `light`
155
+ - `forcedTheme`: Forced theme name for the current page (does not modify saved theme settings)
156
+ - `enableSystem = true`: Whether to switch between `dark` and `light` based on `prefers-color-scheme`
157
+ - `enableColorScheme = true`: Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons
158
+ - `disableTransitionOnChange = false`: Optionally disable all CSS transitions when switching themes ([example](#disable-transitions-on-theme-change))
159
+ - `themes = ['light', 'dark']`: List of theme names
160
+ - `attribute = 'data-theme'`: HTML attribute modified based on the active theme
161
+ - accepts `class` and `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.) ([example](#class-instead-of-data-attribute))
162
+ - `value`: Optional mapping of theme name to attribute value
163
+ - value is an `object` where key is the theme name and value is the attribute value ([example](#differing-dom-attribute-and-theme-name))
164
+ - `nonce`: Optional nonce passed to the injected `script` tag, used to allow-list the ssr-themes script in your CSP
165
+ - `scriptProps`: Optional props to pass to the injected `script` tag ([example](#using-with-cloudflare-rocket-loader))
166
+
167
+ ### registerTheme
168
+
169
+ Use `registerTheme` to generate props for your `<html>` element on the server. When a theme is provided, it applies the theme attribute/class and sets `color-scheme` (if enabled), so the client script treats the existing HTML value as authoritative.
170
+
171
+ ```tsx
172
+ const htmlProps = registerTheme({
173
+ theme: 'dark',
174
+ attribute: 'class',
175
+ });
176
+ ```
177
+
178
+ If `theme` is `undefined` or `'system'`, it returns an empty object.
179
+
180
+ ### useTheme
181
+
182
+ useTheme takes no parameters, but returns:
183
+
184
+ - `theme`: Active theme name
185
+ - `setTheme(name)`: Function to update the theme. The API is identical to the [set function](https://react.dev/reference/react/useState#setstate) returned by `useState`-hook. Pass the new theme value or use a callback to set the new theme based on the current theme.
186
+ - `forcedTheme`: Forced page theme or falsy. If `forcedTheme` is set, you should disable any theme switching UI
187
+ - `resolvedTheme`: If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme`
188
+ - `systemTheme`: If `enableSystem` is true, represents the System theme preference ("dark" or "light"), regardless what the active theme is
189
+ - `themes`: The list of themes passed to `ThemeProvider` (with "system" appended, if `enableSystem` is true)
190
+
191
+ Not too bad, right? Let's see how to use these properties with examples:
192
+
193
+ ## Examples
194
+
195
+ The [Live Example](https://ssr-themes-example.vercel.app/) shows ssr-themes in action, with dark, light, system themes and pages with forced themes.
196
+
197
+ ### Use System preference by default
198
+
199
+ For versions above v0.0.12, the `defaultTheme` is automatically set to "system", so to use System preference you can simply use:
200
+
201
+ ```jsx
202
+ <ThemeProvider>
203
+ ```
204
+
205
+ ### Ignore System preference
206
+
207
+ If you don't want a System theme, disable it via `enableSystem`:
208
+
209
+ ```jsx
210
+ <ThemeProvider enableSystem={false}>
211
+ ```
212
+
213
+ ### Class instead of data attribute
214
+
215
+ If your app uses a class to style the page based on the theme, change the attribute prop to `class`:
216
+
217
+ ```jsx
218
+ <ThemeProvider attribute="class">
219
+ ```
220
+
221
+ Now, setting the theme to "dark" will set `class="dark"` on the `html` element.
222
+
223
+ ### Force page to a theme
224
+
225
+ Let's say your cool new marketing page is dark mode only. The page should always use the dark theme, and changing the theme should have no effect. In TanStack Start, add static data to the route and read it in the root route:
226
+
227
+ ```tsx
228
+ // src/routes/dark.tsx
229
+ import {createFileRoute} from '@tanstack/react-router';
230
+
231
+ export const Route = createFileRoute('/dark')({
232
+ staticData: {theme: 'dark'},
233
+ component: DarkPage,
234
+ });
235
+ ```
236
+
237
+ ```tsx
238
+ // src/routes/__root.tsx
239
+ import { useMatches } from '@tanstack/react-router'
240
+
241
+ const matches = useMatches()
242
+ const forcedTheme = matches.reduce<string | undefined>((theme, match) => {
243
+ const staticData = match.staticData as { theme?: string } | undefined
244
+ return staticData?.theme ?? theme
245
+ }, undefined)
246
+
247
+ <ThemeProvider forcedTheme={forcedTheme} attribute="class">
248
+ {children}
249
+ </ThemeProvider>
250
+ ```
251
+
252
+ Done! Your page is always dark theme (regardless of user preference), and calling `setTheme` from `useTheme` is now a no-op. However, you should make sure to disable any of your UI that would normally change the theme:
253
+
254
+ ```js
255
+ const {forcedTheme} = useTheme();
256
+
257
+ // Theme is forced, we shouldn't allow user to change the theme
258
+ const disabled = !!forcedTheme;
259
+ ```
260
+
261
+ ### Disable transitions on theme change
262
+
263
+ I wrote about [this technique here](https://paco.sh/blog/disable-theme-transitions). We can forcefully disable all CSS transitions before the theme is changed, and re-enable them immediately afterwards. This ensures your UI with different transition durations won't feel inconsistent when changing the theme.
264
+
265
+ To enable this behavior, pass the `disableTransitionOnChange` prop:
266
+
267
+ ```jsx
268
+ <ThemeProvider disableTransitionOnChange>
269
+ ```
270
+
271
+ ### Differing DOM attribute and theme name
272
+
273
+ The name of the active theme is used as both the cookie value and the value of the DOM attribute. If the theme name is "pink", the cookie will contain `theme=pink` and the DOM will be `data-theme="pink"`. You **cannot** modify the cookie value, but you **can** modify the DOM value.
274
+
275
+ If we want the DOM to instead render `data-theme="my-pink-theme"` when the theme is "pink", pass the `value` prop:
276
+
277
+ ```jsx
278
+ <ThemeProvider value={{ pink: 'my-pink-theme' }}>
279
+ ```
280
+
281
+ Done! To be extra clear, this affects only the DOM. Here's how all the values will look:
282
+
283
+ ```js
284
+ const {theme} = useTheme();
285
+ // => "pink"
286
+
287
+ document.cookie;
288
+ // => "theme=pink"
289
+
290
+ document.documentElement.getAttribute('data-theme');
291
+ // => "my-pink-theme"
292
+ ```
293
+
294
+ ### Using with Cloudflare Rocket Loader
295
+
296
+ [Rocket Loader](https://developers.cloudflare.com/fundamentals/speed/rocket-loader/) is a Cloudflare optimization that defers the loading of inline and external scripts to prioritize the website content. Since ssr-themes relies on a script injection to avoid screen flashing on page load, Rocket Loader breaks this functionality. Individual scripts [can be ignored](https://developers.cloudflare.com/fundamentals/speed/rocket-loader/ignore-javascripts/) by adding the `data-cfasync="false"` attribute to the script tag:
297
+
298
+ ```jsx
299
+ <ThemeProvider scriptProps={{ 'data-cfasync': 'false' }}>
300
+ ```
301
+
302
+ ### More than light and dark mode
303
+
304
+ ssr-themes is designed to support any number of themes! Simply pass a list of themes:
305
+
306
+ ```jsx
307
+ <ThemeProvider themes={['pink', 'red', 'blue']}>
308
+ ```
309
+
310
+ > **Note!** When you pass `themes`, the default set of themes ("light" and "dark") are overridden. Make sure you include those if you still want your light and dark themes:
311
+
312
+ ```jsx
313
+ <ThemeProvider themes={['pink', 'red', 'blue', 'light', 'dark']}>
314
+ ```
315
+
316
+ For a starting point, check out `examples/example`.
317
+
318
+ ### Without CSS variables
319
+
320
+ This library does not rely on your theme styling using CSS variables. You can hard-code the values in your CSS, and everything will work as expected (without any flashing):
321
+
322
+ ```css
323
+ html,
324
+ body {
325
+ color: #000;
326
+ background: #fff;
327
+ }
328
+
329
+ [data-theme='dark'],
330
+ [data-theme='dark'] body {
331
+ color: #fff;
332
+ background: #000;
333
+ }
334
+ ```
335
+
336
+ ### With Styled Components and any CSS-in-JS
337
+
338
+ SSR Themes is completely CSS independent, it will work with any library. For example, with Styled Components you can wire `createGlobalStyle` in your root route:
339
+
340
+ ```tsx
341
+ // src/routes/__root.tsx
342
+ import {Outlet, createRootRoute} from '@tanstack/react-router';
343
+ import {createGlobalStyle} from 'styled-components';
344
+ import {ThemeProvider} from 'ssr-themes';
345
+
346
+ // Your themeing variables
347
+ const GlobalStyle = createGlobalStyle`
348
+ :root {
349
+ --fg: #000;
350
+ --bg: #fff;
351
+ }
352
+
353
+ [data-theme="dark"] {
354
+ --fg: #fff;
355
+ --bg: #000;
356
+ }
357
+ `;
358
+
359
+ function RootComponent() {
360
+ return (
361
+ <>
362
+ <GlobalStyle />
363
+ <ThemeProvider>
364
+ <Outlet />
365
+ </ThemeProvider>
366
+ </>
367
+ );
368
+ }
369
+
370
+ export const Route = createRootRoute({
371
+ component: RootComponent,
372
+ });
373
+ ```
374
+
375
+ ### Avoid Hydration Mismatch
376
+
377
+ Because we cannot know the `theme` on the server, many of the values returned from `useTheme` will be `undefined` until mounted on the client. This means if you try to render UI based on the current theme before mounting on the client, you will see a hydration mismatch error.
378
+
379
+ The following code sample is **unsafe**:
380
+
381
+ ```jsx
382
+ import {useTheme} from 'ssr-themes';
383
+
384
+ // Do NOT use this! It will throw a hydration mismatch error.
385
+ const ThemeSwitch = () => {
386
+ const {theme, setTheme} = useTheme();
387
+
388
+ return (
389
+ <select value={theme} onChange={e => setTheme(e.target.value)}>
390
+ <option value="system">System</option>
391
+ <option value="dark">Dark</option>
392
+ <option value="light">Light</option>
393
+ </select>
394
+ );
395
+ };
396
+
397
+ export default ThemeSwitch;
398
+ ```
399
+
400
+ To fix this, make sure you only render UI that uses the current theme when the page is mounted on the client:
401
+
402
+ ```jsx
403
+ import {useState, useEffect} from 'react';
404
+ import {useTheme} from 'ssr-themes';
405
+
406
+ const ThemeSwitch = () => {
407
+ const [mounted, setMounted] = useState(false);
408
+ const {theme, setTheme} = useTheme();
409
+
410
+ // useEffect only runs on the client, so now we can safely show the UI
411
+ useEffect(() => {
412
+ setMounted(true);
413
+ }, []);
414
+
415
+ if (!mounted) {
416
+ return null;
417
+ }
418
+
419
+ return (
420
+ <select value={theme} onChange={e => setTheme(e.target.value)}>
421
+ <option value="system">System</option>
422
+ <option value="dark">Dark</option>
423
+ <option value="light">Light</option>
424
+ </select>
425
+ );
426
+ };
427
+
428
+ export default ThemeSwitch;
429
+ ```
430
+
431
+ Alternatively, you could lazy load the component on the client side with `React.lazy`:
432
+
433
+ ```jsx
434
+ import {Suspense, lazy} from 'react';
435
+
436
+ const ThemeSwitch = lazy(() => import('./ThemeSwitch'));
437
+
438
+ const ThemePage = () => {
439
+ return (
440
+ <Suspense fallback={null}>
441
+ <ThemeSwitch />
442
+ </Suspense>
443
+ );
444
+ };
445
+
446
+ export default ThemePage;
447
+ ```
448
+
449
+ To avoid [Layout Shift](https://web.dev/cls/), consider rendering a skeleton/placeholder until mounted on the client side.
450
+
451
+ #### Images
452
+
453
+ Showing different images based on the current theme also suffers from the hydration mismatch problem. You can use a placeholder image until the theme is resolved:
454
+
455
+ ```jsx
456
+ import {useTheme} from 'ssr-themes';
457
+
458
+ function ThemedImage() {
459
+ const {resolvedTheme} = useTheme();
460
+ let src;
461
+
462
+ switch (resolvedTheme) {
463
+ case 'light':
464
+ src = '/light.png';
465
+ break;
466
+ case 'dark':
467
+ src = '/dark.png';
468
+ break;
469
+ default:
470
+ src =
471
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
472
+ break;
473
+ }
474
+
475
+ return <img src={src} width={400} height={400} alt="" />;
476
+ }
477
+
478
+ export default ThemedImage;
479
+ ```
480
+
481
+ #### CSS
482
+
483
+ You can also use CSS to hide or show content based on the current theme. To avoid the hydration mismatch, you'll need to render _both_ versions of the UI, with CSS hiding the unused version. For example:
484
+
485
+ ```jsx
486
+ function ThemedImage() {
487
+ return (
488
+ <>
489
+ {/* When the theme is dark, hide this div */}
490
+ <div data-hide-on-theme="dark">
491
+ <img src="light.png" width={400} height={400} alt="" />
492
+ </div>
493
+
494
+ {/* When the theme is light, hide this div */}
495
+ <div data-hide-on-theme="light">
496
+ <img src="dark.png" width={400} height={400} alt="" />
497
+ </div>
498
+ </>
499
+ );
500
+ }
501
+
502
+ export default ThemedImage;
503
+ ```
504
+
505
+ ```css
506
+ [data-theme='dark'] [data-hide-on-theme='dark'],
507
+ [data-theme='light'] [data-hide-on-theme='light'] {
508
+ display: none;
509
+ }
510
+ ```
511
+
512
+ ### With TailwindCSS
513
+
514
+ The example in `examples/example` uses Tailwind v4 with a class-based dark mode.
515
+
516
+ Add the dark variant in your CSS:
517
+
518
+ ```css
519
+ @import 'tailwindcss';
520
+ @custom-variant dark (&:where(.dark, .dark *));
521
+ ```
522
+
523
+ Set the attribute for your Theme Provider to `class`:
524
+
525
+ ```tsx
526
+ <ThemeProvider attribute="class">
527
+ ```
528
+
529
+ When the theme is dark, ssr-themes applies the `dark` class to the `html` element:
530
+
531
+ ```html
532
+ <html class="dark">
533
+ <body>
534
+ <div class="bg-white dark:bg-black">
535
+ <!-- ... -->
536
+ </div>
537
+ </body>
538
+ </html>
539
+ ```
540
+
541
+ That's it! Now you can use dark-mode specific classes:
542
+
543
+ ```tsx
544
+ <h1 className="text-black dark:text-white">
545
+ ```
546
+
547
+ ## Discussion
548
+
549
+ ### The Flash
550
+
551
+ ThemeProvider automatically injects a script to update the `html` element with the correct attributes before the rest of your page loads. This means the page will not flash under any circumstances, including forced themes, system theme, multiple themes, and incognito. No `noflash.js` required.
552
+
553
+ ## FAQ
554
+
555
+ ---
556
+
557
+ **Why is my page still flashing?**
558
+
559
+ In dev mode, the page may still flash. When you build your app in production mode, there will be no flashing.
560
+
561
+ ---
562
+
563
+ **Why do I get server/client mismatch error?**
564
+
565
+ When using `useTheme`, you will use see a hydration mismatch error when rendering UI that relies on the current theme. This is because many of the values returned by `useTheme` are undefined on the server, since we can't read cookies until mounting on the client. See the [example](#avoid-hydration-mismatch) for how to fix this error.
566
+
567
+ ---
568
+
569
+ **Do I need to use CSS variables with this library?**
570
+
571
+ Nope. See the [example](#without-css-variables).
572
+
573
+ ---
574
+
575
+ **Can I set the class or data attribute on the body or another element?**
576
+
577
+ Nope. If you have a good reason for supporting this feature, please open an issue.
578
+
579
+ ---
580
+
581
+ **Can I use this package with Gatsby or CRA?**
582
+
583
+ Yes, starting from the 0.3.0 version.
584
+
585
+ ---
586
+
587
+ **Is the injected script minified?**
588
+
589
+ Yes.
590
+
591
+ ---
592
+
593
+ **Why is `resolvedTheme` necessary?**
594
+
595
+ When supporting the System theme preference, you want to make sure that's reflected in your UI. This means your buttons, selects, dropdowns, or whatever you use to indicate the current theme should say "System" when the System theme preference is active.
596
+
597
+ If we didn't distinguish between `theme` and `resolvedTheme`, the UI would show "Dark" or "Light", when it should really be "System".
598
+
599
+ `resolvedTheme` is then useful for modifying behavior or styles at runtime:
600
+
601
+ ```jsx
602
+ const { resolvedTheme } = useTheme()
603
+
604
+ <div style={{ color: resolvedTheme === 'dark' ? 'white' : 'black' }}>
605
+ ```
606
+
607
+ If we didn't have `resolvedTheme` and only used `theme`, you'd lose information about the state of your UI (you would only know the theme is "system", and not what it resolved to).
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+ import { S as SystemThemeDefinition, T as ThemeProviderProps, U as UseThemeProps } from './server-CcYvjBJ6.mjs';
3
+ export { A as Attribute, C as CookieConfig, a as CookieOptions, R as RegisterThemeOptions, b as SystemTheme, c as ThemeHtmlProps, r as registerTheme } from './server-CcYvjBJ6.mjs';
4
+
5
+ declare const useTheme: () => UseThemeProps<SystemThemeDefinition>;
6
+ declare const ThemeProvider: <TThemes extends readonly string[] = SystemThemeDefinition>(props: ThemeProviderProps<TThemes>) => React.JSX.Element;
7
+ declare const ThemeScript: React.MemoExoticComponent<(<TThemes extends readonly string[]>({ forcedTheme, cookieName, attribute, enableSystem, enableColorScheme, defaultTheme, value, themes, nonce, scriptProps, }: Omit<ThemeProviderProps<TThemes>, "children" | "cookie"> & {
8
+ cookieName: string;
9
+ defaultTheme: string;
10
+ }) => React.JSX.Element)>;
11
+
12
+ export { ThemeProvider, ThemeProviderProps, ThemeScript, UseThemeProps, useTheme };
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+ import { S as SystemThemeDefinition, T as ThemeProviderProps, U as UseThemeProps } from './server-CcYvjBJ6.js';
3
+ export { A as Attribute, C as CookieConfig, a as CookieOptions, R as RegisterThemeOptions, b as SystemTheme, c as ThemeHtmlProps, r as registerTheme } from './server-CcYvjBJ6.js';
4
+
5
+ declare const useTheme: () => UseThemeProps<SystemThemeDefinition>;
6
+ declare const ThemeProvider: <TThemes extends readonly string[] = SystemThemeDefinition>(props: ThemeProviderProps<TThemes>) => React.JSX.Element;
7
+ declare const ThemeScript: React.MemoExoticComponent<(<TThemes extends readonly string[]>({ forcedTheme, cookieName, attribute, enableSystem, enableColorScheme, defaultTheme, value, themes, nonce, scriptProps, }: Omit<ThemeProviderProps<TThemes>, "children" | "cookie"> & {
8
+ cookieName: string;
9
+ defaultTheme: string;
10
+ }) => React.JSX.Element)>;
11
+
12
+ export { ThemeProvider, ThemeProviderProps, ThemeScript, UseThemeProps, useTheme };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ "use client";var F=Object.create;var w=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var z=Object.getOwnPropertyNames;var J=Object.getPrototypeOf,q=Object.prototype.hasOwnProperty;var G=(e,t)=>{for(var n in t)w(e,n,{get:t[n],enumerable:!0})},D=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of z(t))!q.call(e,i)&&i!==n&&w(e,i,{get:()=>t[i],enumerable:!(r=W(t,i))||r.enumerable});return e};var K=(e,t,n)=>(n=e!=null?F(J(e)):{},D(t||!e||!e.__esModule?w(n,"default",{value:e,enumerable:!0}):n,e)),X=e=>D(w({},"__esModule",{value:!0}),e);var ue={};G(ue,{ThemeProvider:()=>oe,ThemeScript:()=>j,registerTheme:()=>U,useTheme:()=>ne});module.exports=X(ue);var o=K(require("react"));var $=(e,t,n,r,i,s,a,u)=>{let d=document.documentElement,C=Array.isArray(e)?e:[e],g=[];for(let m of i){let h=s?s[m]:m;h&&g.push({theme:m,value:h})}let N=g.map(m=>m.value),p=m=>{let h=document.cookie.match(new RegExp(`(?:^|; )${m}=([^;]*)`));return h?decodeURIComponent(h[1]):void 0},y=()=>{for(let m of C){if(m==="class"){for(let l of g)if(d.classList.contains(l.value))return l.theme;continue}let h=d.getAttribute(m);if(h){for(let l of g)if(l.value===h)return l.theme}}};function k(m){let h=s?s[m]:m;for(let l of C)l==="class"?(d.classList.remove(...N),h&&d.classList.add(h)):h?d.setAttribute(l,h):d.removeAttribute(l);u&&(m==="light"||m==="dark")&&(d.style.colorScheme=m)}let S=()=>window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";if(r){k(r);return}let R=m=>a&&m==="system"?S():m,v=y();if(v){k(R(v));return}try{let m=p(t)||n;k(R(m))}catch(m){}};var U=({theme:e,attribute:t="class",value:n,enableColorScheme:r=!0}={})=>{if(!e||e==="system")return{};let i=n?n[e]:e;if(!i)return{};let s={},a=Array.isArray(t)?t:[t];for(let u of a)u==="class"?s.className=i:s[u]=i;return r&&(e==="light"||e==="dark")&&(s.style={colorScheme:e}),s};var V=["light","dark"],I="(prefers-color-scheme: dark)",Q=typeof window=="undefined",O=o.createContext(void 0),Y={setTheme:e=>{},themes:[]},Z={path:"/",maxAge:31536e3,sameSite:"lax"},ee=e=>{var t;return(t=e==null?void 0:e.name)!=null?t:"theme"},te=e=>{if(Q)return;let t=document.cookie?document.cookie.split("; "):[];for(let n of t){let[r,...i]=n.split("=");if(r===e)return decodeURIComponent(i.join("="))}},se=e=>{if(e)return`${e.charAt(0).toUpperCase()}${e.slice(1)}`},H=(e,t,n)=>{try{let r={...Z,...n!=null?n:{}},i=encodeURIComponent(t),s=[`${e}=${i}`];r.path&&s.push(`Path=${r.path}`),r.domain&&s.push(`Domain=${r.domain}`),typeof r.maxAge=="number"&&s.push(`Max-Age=${r.maxAge}`),r.expires&&s.push(`Expires=${r.expires.toUTCString()}`);let a=se(r.sameSite);a&&s.push(`SameSite=${a}`),r.secure&&s.push("Secure"),document.cookie=s.join("; ")}catch(r){}},ne=()=>{var e;return(e=o.useContext(O))!=null?e:Y},oe=e=>o.useContext(O)?o.createElement(o.Fragment,null,e.children):o.createElement(ie,{...e}),re=["dark","light"],ie=({forcedTheme:e,disableTransitionOnChange:t=!1,enableSystem:n=!0,enableColorScheme:r=!0,cookie:i,themes:s=re,defaultTheme:a=n?"system":"light",attribute:u="class",value:d,children:C,nonce:g,scriptProps:N})=>{let p=ee(i),[y,k]=o.useState(()=>ae(p,a,u,d,s)),[S,R]=o.useState(()=>y==="system"?E():y),v=d?Object.values(d):s,m=o.useRef(null),h=o.useCallback(c=>{let f=c;if(!f)return;c==="system"&&n&&(f=E());let T=d?d[f]:f,M=t?ce(g):null,x=document.documentElement,L=A=>{A==="class"?(x.classList.remove(...v),T&&x.classList.add(T)):A.startsWith("data-")&&(T?x.setAttribute(A,T):x.removeAttribute(A))};if(Array.isArray(u)?u.forEach(L):L(u),r){let A=V.includes(a)?a:null,_=V.includes(f)?f:A;x.style.colorScheme=_}M==null||M()},[g]),l=o.useCallback(c=>{var f;(f=m.current)==null||f.postMessage({key:p,value:c})},[p]),b=o.useCallback(c=>{typeof c=="function"?k(f=>{let T=c(f);return H(p,T,i),l(T),T}):(k(c),H(p,c,i),l(c))},[p,i,l]),P=o.useCallback(c=>{let f=E(c);R(f),y==="system"&&n&&!e&&h("system")},[y,e]);o.useEffect(()=>{let c=window.matchMedia(I);return c.addListener(P),P(c),()=>c.removeListener(P)},[P]),o.useEffect(()=>{if(typeof window=="undefined"||typeof window.BroadcastChannel=="undefined")return;let c=new BroadcastChannel(`ssr-themes:${p}`);m.current=c;let f=T=>{if(!(!T.data||T.data.key!==p)){if(!T.data.value){k(a);return}k(T.data.value)}};return c.addEventListener("message",f),()=>{c.removeEventListener("message",f),c.close(),m.current===c&&(m.current=null)}},[p,a]),o.useEffect(()=>{h(e!=null?e:y)},[e,y]);let B=o.useMemo(()=>({theme:y,setTheme:b,forcedTheme:e,resolvedTheme:y==="system"?S:y,themes:n?[...s,"system"]:s,systemTheme:n?S:void 0}),[y,b,e,S,n,s]);return o.createElement(O.Provider,{value:B},o.createElement(j,{forcedTheme:e,cookieName:p,attribute:u,enableSystem:n,enableColorScheme:r,defaultTheme:a,value:d,themes:s,nonce:g,scriptProps:N}),C)},j=o.memo(({forcedTheme:e,cookieName:t,attribute:n,enableSystem:r,enableColorScheme:i,defaultTheme:s,value:a,themes:u,nonce:d,scriptProps:C})=>{let g=JSON.stringify([n,t,s,e,u,a,r,i]).slice(1,-1);return o.createElement("script",{...C,suppressHydrationWarning:!0,nonce:typeof window=="undefined"?d:"",dangerouslySetInnerHTML:{__html:`(${$.toString()})(${g})`}})});function me(e,t,n){let r=Array.isArray(e)?e:[e],i=[];for(let s of n){let a=t?t[s]:s;a&&i.push({theme:s,value:a})}for(let s of r){if(s==="class"){for(let u of i)if(document.documentElement.classList.contains(u.value))return u.theme;continue}let a=document.documentElement.getAttribute(s);if(a){for(let u of i)if(u.value===a)return u.theme}}}function ae(e,t,n,r,i){if(Q)return;let s=me(n,r,i);if(s)return s;let a;try{a=te(e)}catch(u){}return a||t}var ce=e=>{let t=document.createElement("style");return e&&t.setAttribute("nonce",e),t.appendChild(document.createTextNode("*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}")),document.head.appendChild(t),()=>{window.getComputedStyle(document.body),setTimeout(()=>{document.head.removeChild(t)},1)}},E=e=>(e||(e=window.matchMedia(I)),e.matches?"dark":"light");0&&(module.exports={ThemeProvider,ThemeScript,registerTheme,useTheme});
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ "use client";import*as s from"react";var L=(e,n,i,o,a,t,m,u)=>{let d=document.documentElement,C=Array.isArray(e)?e:[e],g=[];for(let r of a){let h=t?t[r]:r;h&&g.push({theme:r,value:h})}let w=g.map(r=>r.value),p=r=>{let h=document.cookie.match(new RegExp(`(?:^|; )${r}=([^;]*)`));return h?decodeURIComponent(h[1]):void 0},y=()=>{for(let r of C){if(r==="class"){for(let l of g)if(d.classList.contains(l.value))return l.theme;continue}let h=d.getAttribute(r);if(h){for(let l of g)if(l.value===h)return l.theme}}};function k(r){let h=t?t[r]:r;for(let l of C)l==="class"?(d.classList.remove(...w),h&&d.classList.add(h)):h?d.setAttribute(l,h):d.removeAttribute(l);u&&(r==="light"||r==="dark")&&(d.style.colorScheme=r)}let S=()=>window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";if(o){k(o);return}let R=r=>m&&r==="system"?S():r,v=y();if(v){k(R(v));return}try{let r=p(n)||i;k(R(r))}catch(r){}};var Q=({theme:e,attribute:n="class",value:i,enableColorScheme:o=!0}={})=>{if(!e||e==="system")return{};let a=i?i[e]:e;if(!a)return{};let t={},m=Array.isArray(n)?n:[n];for(let u of m)u==="class"?t.className=a:t[u]=a;return o&&(e==="light"||e==="dark")&&(t.style={colorScheme:e}),t};var D=["light","dark"],U="(prefers-color-scheme: dark)",V=typeof window=="undefined",E=s.createContext(void 0),j={setTheme:e=>{},themes:[]},B={path:"/",maxAge:31536e3,sameSite:"lax"},_=e=>{var n;return(n=e==null?void 0:e.name)!=null?n:"theme"},F=e=>{if(V)return;let n=document.cookie?document.cookie.split("; "):[];for(let i of n){let[o,...a]=i.split("=");if(o===e)return decodeURIComponent(a.join("="))}},W=e=>{if(e)return`${e.charAt(0).toUpperCase()}${e.slice(1)}`},$=(e,n,i)=>{try{let o={...B,...i!=null?i:{}},a=encodeURIComponent(n),t=[`${e}=${a}`];o.path&&t.push(`Path=${o.path}`),o.domain&&t.push(`Domain=${o.domain}`),typeof o.maxAge=="number"&&t.push(`Max-Age=${o.maxAge}`),o.expires&&t.push(`Expires=${o.expires.toUTCString()}`);let m=W(o.sameSite);m&&t.push(`SameSite=${m}`),o.secure&&t.push("Secure"),document.cookie=t.join("; ")}catch(o){}},te=()=>{var e;return(e=s.useContext(E))!=null?e:j},se=e=>s.useContext(E)?s.createElement(s.Fragment,null,e.children):s.createElement(J,{...e}),z=["dark","light"],J=({forcedTheme:e,disableTransitionOnChange:n=!1,enableSystem:i=!0,enableColorScheme:o=!0,cookie:a,themes:t=z,defaultTheme:m=i?"system":"light",attribute:u="class",value:d,children:C,nonce:g,scriptProps:w})=>{let p=_(a),[y,k]=s.useState(()=>K(p,m,u,d,t)),[S,R]=s.useState(()=>y==="system"?M():y),v=d?Object.values(d):t,r=s.useRef(null),h=s.useCallback(c=>{let f=c;if(!f)return;c==="system"&&i&&(f=M());let T=d?d[f]:f,N=n?X(g):null,x=document.documentElement,b=A=>{A==="class"?(x.classList.remove(...v),T&&x.classList.add(T)):A.startsWith("data-")&&(T?x.setAttribute(A,T):x.removeAttribute(A))};if(Array.isArray(u)?u.forEach(b):b(u),o){let A=D.includes(m)?m:null,I=D.includes(f)?f:A;x.style.colorScheme=I}N==null||N()},[g]),l=s.useCallback(c=>{var f;(f=r.current)==null||f.postMessage({key:p,value:c})},[p]),O=s.useCallback(c=>{typeof c=="function"?k(f=>{let T=c(f);return $(p,T,a),l(T),T}):(k(c),$(p,c,a),l(c))},[p,a,l]),P=s.useCallback(c=>{let f=M(c);R(f),y==="system"&&i&&!e&&h("system")},[y,e]);s.useEffect(()=>{let c=window.matchMedia(U);return c.addListener(P),P(c),()=>c.removeListener(P)},[P]),s.useEffect(()=>{if(typeof window=="undefined"||typeof window.BroadcastChannel=="undefined")return;let c=new BroadcastChannel(`ssr-themes:${p}`);r.current=c;let f=T=>{if(!(!T.data||T.data.key!==p)){if(!T.data.value){k(m);return}k(T.data.value)}};return c.addEventListener("message",f),()=>{c.removeEventListener("message",f),c.close(),r.current===c&&(r.current=null)}},[p,m]),s.useEffect(()=>{h(e!=null?e:y)},[e,y]);let H=s.useMemo(()=>({theme:y,setTheme:O,forcedTheme:e,resolvedTheme:y==="system"?S:y,themes:i?[...t,"system"]:t,systemTheme:i?S:void 0}),[y,O,e,S,i,t]);return s.createElement(E.Provider,{value:H},s.createElement(q,{forcedTheme:e,cookieName:p,attribute:u,enableSystem:i,enableColorScheme:o,defaultTheme:m,value:d,themes:t,nonce:g,scriptProps:w}),C)},q=s.memo(({forcedTheme:e,cookieName:n,attribute:i,enableSystem:o,enableColorScheme:a,defaultTheme:t,value:m,themes:u,nonce:d,scriptProps:C})=>{let g=JSON.stringify([i,n,t,e,u,m,o,a]).slice(1,-1);return s.createElement("script",{...C,suppressHydrationWarning:!0,nonce:typeof window=="undefined"?d:"",dangerouslySetInnerHTML:{__html:`(${L.toString()})(${g})`}})});function G(e,n,i){let o=Array.isArray(e)?e:[e],a=[];for(let t of i){let m=n?n[t]:t;m&&a.push({theme:t,value:m})}for(let t of o){if(t==="class"){for(let u of a)if(document.documentElement.classList.contains(u.value))return u.theme;continue}let m=document.documentElement.getAttribute(t);if(m){for(let u of a)if(u.value===m)return u.theme}}}function K(e,n,i,o,a){if(V)return;let t=G(i,o,a);if(t)return t;let m;try{m=F(e)}catch(u){}return m||n}var X=e=>{let n=document.createElement("style");return e&&n.setAttribute("nonce",e),n.appendChild(document.createTextNode("*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}")),document.head.appendChild(n),()=>{window.getComputedStyle(document.body),setTimeout(()=>{document.head.removeChild(n)},1)}},M=e=>(e||(e=window.matchMedia(U)),e.matches?"dark":"light");export{se as ThemeProvider,q as ThemeScript,Q as registerTheme,te as useTheme};
@@ -0,0 +1,87 @@
1
+ import * as React from 'react';
2
+
3
+ interface ValueObject {
4
+ [themeName: string]: string;
5
+ }
6
+ interface CookieOptions {
7
+ /** Cookie path attribute */
8
+ path?: string;
9
+ /** Cookie max-age attribute in seconds */
10
+ maxAge?: number;
11
+ /** Cookie expires attribute */
12
+ expires?: Date;
13
+ /** Cookie SameSite attribute */
14
+ sameSite?: 'lax' | 'strict' | 'none';
15
+ /** Cookie domain attribute */
16
+ domain?: string;
17
+ /** Cookie secure attribute */
18
+ secure?: boolean;
19
+ }
20
+ interface CookieConfig extends CookieOptions {
21
+ /** Cookie name used to store theme preference */
22
+ name?: string;
23
+ }
24
+ type SystemThemeDefinition = readonly ['dark', 'light'];
25
+ type SystemTheme = SystemThemeDefinition[number];
26
+ type DataAttribute = `data-${string}`;
27
+ interface ScriptProps extends React.DetailedHTMLProps<React.ScriptHTMLAttributes<HTMLScriptElement>, HTMLScriptElement> {
28
+ [dataAttribute: DataAttribute]: any;
29
+ }
30
+ interface UseThemeProps<TThemes extends readonly string[] = SystemThemeDefinition> {
31
+ /** List of all available theme names */
32
+ themes: TThemes;
33
+ /** Forced theme name for the current page */
34
+ forcedTheme?: ThemeName<TThemes> | undefined;
35
+ /** Update the theme */
36
+ setTheme: React.Dispatch<React.SetStateAction<string>>;
37
+ /** Active theme name */
38
+ theme?: ThemeName<TThemes> | undefined;
39
+ /** If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme` */
40
+ resolvedTheme?: ThemeName<TThemes> | undefined;
41
+ /** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */
42
+ systemTheme?: SystemTheme | undefined;
43
+ }
44
+ type Attribute = DataAttribute | 'class';
45
+ type ThemeName<T extends readonly string[]> = T[number];
46
+ type ThemeHtmlProps = {
47
+ className?: string;
48
+ style?: React.CSSProperties;
49
+ } & Partial<Record<DataAttribute, string>>;
50
+ interface RegisterThemeOptions<TThemes extends readonly string[] = SystemThemeDefinition> {
51
+ /** Resolved theme name to apply to the html element */
52
+ theme?: ThemeName<TThemes> | undefined;
53
+ /** HTML attribute modified based on the active theme */
54
+ attribute?: Attribute | Attribute[] | undefined;
55
+ /** Mapping of theme name to HTML attribute value */
56
+ value?: ValueObject | undefined;
57
+ /** Whether to indicate to browsers which color scheme is used */
58
+ enableColorScheme?: boolean | undefined;
59
+ }
60
+ interface ThemeProviderProps<TThemes extends readonly string[] = SystemThemeDefinition> extends React.PropsWithChildren<unknown> {
61
+ /** List of all available theme names */
62
+ themes?: TThemes | undefined;
63
+ /** Forced theme name for the current page */
64
+ forcedTheme?: ThemeName<TThemes> | undefined;
65
+ /** Whether to switch between dark and light themes based on prefers-color-scheme */
66
+ enableSystem?: boolean | undefined;
67
+ /** Disable all CSS transitions when switching themes */
68
+ disableTransitionOnChange?: boolean | undefined;
69
+ /** Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons */
70
+ enableColorScheme?: boolean | undefined;
71
+ /** Cookie configuration used to store theme preference */
72
+ cookie?: CookieConfig | undefined;
73
+ /** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */
74
+ defaultTheme?: ThemeName<TThemes> | undefined;
75
+ /** HTML attribute modified based on the active theme. Accepts `class`, `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.), or an array which could include both */
76
+ attribute?: Attribute | Attribute[] | undefined;
77
+ /** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
78
+ value?: ValueObject | undefined;
79
+ /** Nonce string to pass to the inline script and style elements for CSP headers */
80
+ nonce?: string;
81
+ /** Props to pass the inline script */
82
+ scriptProps?: ScriptProps;
83
+ }
84
+
85
+ declare const registerTheme: <TThemes extends readonly string[] = SystemThemeDefinition>({ theme, attribute, value, enableColorScheme, }?: RegisterThemeOptions<TThemes>) => ThemeHtmlProps;
86
+
87
+ export { type Attribute as A, type CookieConfig as C, type RegisterThemeOptions as R, type SystemThemeDefinition as S, type ThemeProviderProps as T, type UseThemeProps as U, type CookieOptions as a, type SystemTheme as b, type ThemeHtmlProps as c, registerTheme as r };
@@ -0,0 +1,87 @@
1
+ import * as React from 'react';
2
+
3
+ interface ValueObject {
4
+ [themeName: string]: string;
5
+ }
6
+ interface CookieOptions {
7
+ /** Cookie path attribute */
8
+ path?: string;
9
+ /** Cookie max-age attribute in seconds */
10
+ maxAge?: number;
11
+ /** Cookie expires attribute */
12
+ expires?: Date;
13
+ /** Cookie SameSite attribute */
14
+ sameSite?: 'lax' | 'strict' | 'none';
15
+ /** Cookie domain attribute */
16
+ domain?: string;
17
+ /** Cookie secure attribute */
18
+ secure?: boolean;
19
+ }
20
+ interface CookieConfig extends CookieOptions {
21
+ /** Cookie name used to store theme preference */
22
+ name?: string;
23
+ }
24
+ type SystemThemeDefinition = readonly ['dark', 'light'];
25
+ type SystemTheme = SystemThemeDefinition[number];
26
+ type DataAttribute = `data-${string}`;
27
+ interface ScriptProps extends React.DetailedHTMLProps<React.ScriptHTMLAttributes<HTMLScriptElement>, HTMLScriptElement> {
28
+ [dataAttribute: DataAttribute]: any;
29
+ }
30
+ interface UseThemeProps<TThemes extends readonly string[] = SystemThemeDefinition> {
31
+ /** List of all available theme names */
32
+ themes: TThemes;
33
+ /** Forced theme name for the current page */
34
+ forcedTheme?: ThemeName<TThemes> | undefined;
35
+ /** Update the theme */
36
+ setTheme: React.Dispatch<React.SetStateAction<string>>;
37
+ /** Active theme name */
38
+ theme?: ThemeName<TThemes> | undefined;
39
+ /** If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme` */
40
+ resolvedTheme?: ThemeName<TThemes> | undefined;
41
+ /** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */
42
+ systemTheme?: SystemTheme | undefined;
43
+ }
44
+ type Attribute = DataAttribute | 'class';
45
+ type ThemeName<T extends readonly string[]> = T[number];
46
+ type ThemeHtmlProps = {
47
+ className?: string;
48
+ style?: React.CSSProperties;
49
+ } & Partial<Record<DataAttribute, string>>;
50
+ interface RegisterThemeOptions<TThemes extends readonly string[] = SystemThemeDefinition> {
51
+ /** Resolved theme name to apply to the html element */
52
+ theme?: ThemeName<TThemes> | undefined;
53
+ /** HTML attribute modified based on the active theme */
54
+ attribute?: Attribute | Attribute[] | undefined;
55
+ /** Mapping of theme name to HTML attribute value */
56
+ value?: ValueObject | undefined;
57
+ /** Whether to indicate to browsers which color scheme is used */
58
+ enableColorScheme?: boolean | undefined;
59
+ }
60
+ interface ThemeProviderProps<TThemes extends readonly string[] = SystemThemeDefinition> extends React.PropsWithChildren<unknown> {
61
+ /** List of all available theme names */
62
+ themes?: TThemes | undefined;
63
+ /** Forced theme name for the current page */
64
+ forcedTheme?: ThemeName<TThemes> | undefined;
65
+ /** Whether to switch between dark and light themes based on prefers-color-scheme */
66
+ enableSystem?: boolean | undefined;
67
+ /** Disable all CSS transitions when switching themes */
68
+ disableTransitionOnChange?: boolean | undefined;
69
+ /** Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons */
70
+ enableColorScheme?: boolean | undefined;
71
+ /** Cookie configuration used to store theme preference */
72
+ cookie?: CookieConfig | undefined;
73
+ /** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */
74
+ defaultTheme?: ThemeName<TThemes> | undefined;
75
+ /** HTML attribute modified based on the active theme. Accepts `class`, `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.), or an array which could include both */
76
+ attribute?: Attribute | Attribute[] | undefined;
77
+ /** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
78
+ value?: ValueObject | undefined;
79
+ /** Nonce string to pass to the inline script and style elements for CSP headers */
80
+ nonce?: string;
81
+ /** Props to pass the inline script */
82
+ scriptProps?: ScriptProps;
83
+ }
84
+
85
+ declare const registerTheme: <TThemes extends readonly string[] = SystemThemeDefinition>({ theme, attribute, value, enableColorScheme, }?: RegisterThemeOptions<TThemes>) => ThemeHtmlProps;
86
+
87
+ export { type Attribute as A, type CookieConfig as C, type RegisterThemeOptions as R, type SystemThemeDefinition as S, type ThemeProviderProps as T, type UseThemeProps as U, type CookieOptions as a, type SystemTheme as b, type ThemeHtmlProps as c, registerTheme as r };
@@ -0,0 +1,2 @@
1
+ export { A as Attribute, C as CookieConfig, a as CookieOptions, R as RegisterThemeOptions, b as SystemTheme, c as ThemeHtmlProps, T as ThemeProviderProps, U as UseThemeProps, r as registerTheme } from './server-CcYvjBJ6.mjs';
2
+ import 'react';
@@ -0,0 +1,2 @@
1
+ export { A as Attribute, C as CookieConfig, a as CookieOptions, R as RegisterThemeOptions, b as SystemTheme, c as ThemeHtmlProps, T as ThemeProviderProps, U as UseThemeProps, r as registerTheme } from './server-CcYvjBJ6.js';
2
+ import 'react';
package/dist/server.js ADDED
@@ -0,0 +1 @@
1
+ var m=Object.defineProperty;var f=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var l=(e,s)=>{for(var o in s)m(e,o,{get:s[o],enumerable:!0})},c=(e,s,o,t)=>{if(s&&typeof s=="object"||typeof s=="function")for(let r of h(s))!y.call(e,r)&&r!==o&&m(e,r,{get:()=>s[r],enumerable:!(t=f(s,r))||t.enumerable});return e};var a=e=>c(m({},"__esModule",{value:!0}),e);var g={};l(g,{registerTheme:()=>p});module.exports=a(g);var p=({theme:e,attribute:s="class",value:o,enableColorScheme:t=!0}={})=>{if(!e||e==="system")return{};let r=o?o[e]:e;if(!r)return{};let i={},T=Array.isArray(s)?s:[s];for(let n of T)n==="class"?i.className=r:i[n]=r;return t&&(e==="light"||e==="dark")&&(i.style={colorScheme:e}),i};0&&(module.exports={registerTheme});
@@ -0,0 +1 @@
1
+ var p=({theme:e,attribute:r="class",value:t,enableColorScheme:m=!0}={})=>{if(!e||e==="system")return{};let o=t?t[e]:e;if(!o)return{};let s={},n=Array.isArray(r)?r:[r];for(let i of n)i==="class"?s.className=o:s[i]=o;return m&&(e==="light"||e==="dark")&&(s.style={colorScheme:e}),s};export{p as registerTheme};
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "ssr-themes",
3
+ "version": "0.0.1-alpha.1",
4
+ "license": "Apache-2.0",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "LICENSE.md",
11
+ "package.json"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "react-server": "./dist/server.mjs",
17
+ "import": "./dist/index.mjs",
18
+ "require": "./dist/index.js"
19
+ },
20
+ "./server": {
21
+ "types": "./dist/server.d.ts",
22
+ "import": "./dist/server.mjs",
23
+ "require": "./dist/server.js"
24
+ }
25
+ },
26
+ "scripts": {
27
+ "prepublish": "bun run build",
28
+ "build": "tsup",
29
+ "dev": "tsup --watch",
30
+ "test": "vitest run __tests__",
31
+ "clean": "rm -rf dist node_modules"
32
+ },
33
+ "peerDependencies": {
34
+ "react": "^19.2.3",
35
+ "react-dom": "^19.2.3"
36
+ },
37
+ "devDependencies": {
38
+ "react": "^19.2.3",
39
+ "react-dom": "^19.2.3"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/0xcadams/ssr-themes.git"
44
+ }
45
+ }