react-theme-tide 0.2.1 → 0.2.5
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 +42 -145
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# React Theme Tide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Switch your site between light and dark with a wave of water. The new theme rises from the bottom of the screen, pauses for a second with the waves still moving, then fills the page.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<p>
|
|
6
|
+
<a href="https://github.com/alawonn/react-theme-tide/blob/main/docs/images/tide-1-rise.png"><img src="https://raw.githubusercontent.com/alawonn/react-theme-tide/main/docs/images/tide-1-rise.png" width="260" alt="The dark theme rising from the bottom of the demo page"></a>
|
|
7
|
+
<a href="https://github.com/alawonn/react-theme-tide/blob/main/docs/images/tide-2-pause.png"><img src="https://raw.githubusercontent.com/alawonn/react-theme-tide/main/docs/images/tide-2-pause.png" width="260" alt="The water pausing at 60% of the screen"></a>
|
|
8
|
+
<a href="https://github.com/alawonn/react-theme-tide/blob/main/docs/images/tide-3-fill.png"><img src="https://raw.githubusercontent.com/alawonn/react-theme-tide/main/docs/images/tide-3-fill.png" width="260" alt="The water about to fill the page"></a>
|
|
9
|
+
</p>
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
**[Try the demo](https://alawonn.github.io/react-theme-tide/)**
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
You keep your own theme code. React Theme Tide only animates the change.
|
|
10
14
|
|
|
11
15
|
## Install
|
|
12
16
|
|
|
@@ -14,89 +18,38 @@ React Theme Tide doesn't store your theme. You keep your own theme state and sto
|
|
|
14
18
|
npm install react-theme-tide
|
|
15
19
|
```
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## Quick start
|
|
20
|
-
|
|
21
|
-
Pass the function that changes your theme. It must update the DOM before it returns (or before its promise resolves), because the browser captures the new page right after it.
|
|
22
|
-
|
|
23
|
-
```tsx
|
|
24
|
-
"use client";
|
|
25
|
-
import { useThemeTide } from "react-theme-tide";
|
|
26
|
-
|
|
27
|
-
export function ThemeToggle() {
|
|
28
|
-
const tide = useThemeTide();
|
|
29
|
-
const toggle = () =>
|
|
30
|
-
tide.run(() => {
|
|
31
|
-
const dark = document.documentElement.classList.toggle("dark");
|
|
32
|
-
localStorage.setItem("theme", dark ? "dark" : "light");
|
|
33
|
-
});
|
|
34
|
-
return (
|
|
35
|
-
<button type="button" onClick={toggle} aria-label="Toggle theme">
|
|
36
|
-
Toggle theme
|
|
37
|
-
</button>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add react-theme-tide
|
|
40
23
|
```
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
import { themeTide } from "react-theme-tide";
|
|
46
|
-
|
|
47
|
-
themeTide(() => document.documentElement.classList.toggle("dark"));
|
|
25
|
+
```bash
|
|
26
|
+
yarn add react-theme-tide
|
|
48
27
|
```
|
|
49
28
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
If your site already has a function that applies and saves the theme, pass it in unchanged:
|
|
53
|
-
|
|
54
|
-
```tsx
|
|
55
|
-
"use client";
|
|
56
|
-
import { useThemeTide } from "react-theme-tide";
|
|
57
|
-
import { setAppearance } from "@/lib/appearance"; // your existing setter
|
|
58
|
-
|
|
59
|
-
export function AppearanceButtons() {
|
|
60
|
-
const tide = useThemeTide();
|
|
61
|
-
return (
|
|
62
|
-
<>
|
|
63
|
-
<button type="button" onClick={() => tide.run(() => setAppearance("light"))}>Light</button>
|
|
64
|
-
<button type="button" onClick={() => tide.run(() => setAppearance("dark"))}>Dark</button>
|
|
65
|
-
</>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
29
|
+
```bash
|
|
30
|
+
bun add react-theme-tide
|
|
68
31
|
```
|
|
69
32
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
### Theme held in React state
|
|
73
|
-
|
|
74
|
-
React state updates are asynchronous, so wrap them in `flushSync`. That way the DOM has the new theme when the callback returns:
|
|
75
|
-
|
|
76
|
-
```tsx
|
|
77
|
-
import { flushSync } from "react-dom";
|
|
78
|
-
|
|
79
|
-
tide.run(() => flushSync(() => setTheme("dark")));
|
|
33
|
+
```bash
|
|
34
|
+
deno add npm:react-theme-tide
|
|
80
35
|
```
|
|
81
36
|
|
|
82
|
-
|
|
37
|
+
You need React 18 or newer. There's no CSS to import.
|
|
83
38
|
|
|
84
|
-
##
|
|
39
|
+
## Use it
|
|
85
40
|
|
|
86
|
-
|
|
41
|
+
Wrap the code that changes your theme in `run`:
|
|
87
42
|
|
|
88
43
|
```tsx
|
|
89
|
-
// app/theme-toggle.tsx
|
|
90
44
|
"use client";
|
|
91
45
|
import { useThemeTide } from "react-theme-tide";
|
|
92
46
|
|
|
93
47
|
export function ThemeToggle() {
|
|
94
|
-
const
|
|
48
|
+
const tide = useThemeTide();
|
|
95
49
|
return (
|
|
96
50
|
<button
|
|
97
51
|
type="button"
|
|
98
|
-
|
|
99
|
-
onClick={() => run(() => document.documentElement.classList.toggle("dark"))}
|
|
52
|
+
onClick={() => tide.run(() => document.documentElement.classList.toggle("dark"))}
|
|
100
53
|
>
|
|
101
54
|
Switch theme
|
|
102
55
|
</button>
|
|
@@ -104,95 +57,39 @@ export function ThemeToggle() {
|
|
|
104
57
|
}
|
|
105
58
|
```
|
|
106
59
|
|
|
107
|
-
|
|
108
|
-
// app/layout.tsx (a Server Component)
|
|
109
|
-
import { ThemeToggle } from "./theme-toggle";
|
|
60
|
+
Already have a function that sets and saves the theme? Pass it in as it is:
|
|
110
61
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<html lang="en" suppressHydrationWarning>
|
|
114
|
-
<body>
|
|
115
|
-
<ThemeToggle />
|
|
116
|
-
{children}
|
|
117
|
-
</body>
|
|
118
|
-
</html>
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Apply the saved theme before paint the way you do now, for example with an inline script in `<head>`. The tide only runs when someone changes the theme, never on first load or during hydration.
|
|
124
|
-
|
|
125
|
-
## Options
|
|
126
|
-
|
|
127
|
-
All options are optional. Pass them to `useThemeTide(options)`, to `run(commit, options)` (which overrides the hook's options) or to `themeTide(commit, options)`.
|
|
128
|
-
|
|
129
|
-
| Option | Default | Range | What it does |
|
|
130
|
-
| --- | --- | --- | --- |
|
|
131
|
-
| `riseDuration` | `900` | 0 to 10000 ms | Time to rise from the bottom edge to the hold level. |
|
|
132
|
-
| `holdDuration` | `1000` | 0 to 10000 ms | Time the water stays at the hold level. The waves keep moving. |
|
|
133
|
-
| `finishDuration` | `700` | 0 to 10000 ms | Time to rise from the hold level past the top edge. |
|
|
134
|
-
| `holdLevel` | `0.6` | 0 to 1 | How full the screen is during the hold. `0.6` puts the mean waterline 40% down from the top. |
|
|
135
|
-
| `waveAmplitude` | `12` | 0 to 80 px | Height of a crest above the mean waterline. `0` gives a flat line. |
|
|
136
|
-
| `waveLength` | `240` | 40 to 4000 px | Distance between crests. |
|
|
137
|
-
| `waveSpeed` | `1` | 0 to 5 | Crests that pass a fixed point each second. |
|
|
138
|
-
| `respectReducedMotion` | `true` | boolean | Change the theme instantly when the user's system asks for reduced motion. |
|
|
139
|
-
| `nonce` | none | string | Nonce for the temporary `<style>` element, for pages with a Content Security Policy. |
|
|
140
|
-
|
|
141
|
-
A value outside its range, or of the wrong type, makes the call reject with a `RangeError` or `TypeError`. The theme still changes, instantly, so a bad option never breaks the toggle. To check options ahead of time, call `resolveThemeTideOptions(options)`, which throws the same errors.
|
|
142
|
-
|
|
143
|
-
## How it behaves
|
|
144
|
-
|
|
145
|
-
- **Coordinates.** The water is measured against the visible screen, not the document. It always starts at the bottom edge of the screen, however long the page is or however far it's scrolled. Scroll position and focus stay as they were.
|
|
146
|
-
- **Both directions.** Light to dark and dark to light both fill upward.
|
|
147
|
-
- **Completion.** At the end, the troughs pass above the top edge and the fill reaches past both sides, so none of the old theme is left showing.
|
|
148
|
-
- **Your commit runs once.** Every call runs your function exactly once, whether the tide animates, falls back, finishes early or fails.
|
|
149
|
-
- **Another change during a tide.** The running tide jumps to its end, then the new theme applies instantly. Changes always apply in the order they were made.
|
|
150
|
-
- **Scrolling during a tide.** The water rushes to the top in about 0.2 s, with the waves still moving, and the page ends up where you scrolled. The old theme is a still snapshot, so it can't scroll along with the page.
|
|
151
|
-
- **Resizing during a tide.** The tide ends right away, because the snapshot no longer fits the window.
|
|
152
|
-
- **Input.** Keyboard input reaches the focused control during a tide. A mouse or touch press makes the water rush to the top, the same as scrolling. In Firefox the press also reaches the control under it. In Chromium it doesn't, because the browser sends pointer events to the root element during any View Transition, so the next press works as usual.
|
|
153
|
-
- **Instant fallback.** The theme changes with no animation when the browser lacks View Transitions or CSS masks, when reduced motion applies, when the tab is hidden, when every duration is `0`, or when another View Transition is already running (in browsers that report `document.activeViewTransition`).
|
|
154
|
-
- **Errors.** If your function throws or rejects, the returned promise rejects with that error. The animation and its temporary style are removed either way.
|
|
155
|
-
- **Unmounting.** Unmounting a component that started a tide doesn't stop the tide. Nothing stays behind once it ends.
|
|
156
|
-
|
|
157
|
-
The returned promise resolves to `{ animated: true }`, or to `{ animated: false, reason }`, where `reason` is one of `"reduced-motion"`, `"unsupported"`, `"hidden"`, `"zero-duration"`, `"busy"` or `"interrupted"`.
|
|
158
|
-
|
|
159
|
-
## API
|
|
160
|
-
|
|
161
|
-
```ts
|
|
162
|
-
themeTide(commit: () => void | Promise<void>, options?: ThemeTideOptions): Promise<ThemeTideResult>
|
|
163
|
-
useThemeTide(options?: ThemeTideOptions): { run, running, skip }
|
|
164
|
-
skipThemeTide(): void // jump a running tide to its end
|
|
165
|
-
isThemeTideRunning(): boolean
|
|
166
|
-
subscribeThemeTide(listener: () => void): () => void
|
|
167
|
-
supportsThemeTide(): boolean // false on the server
|
|
168
|
-
resolveThemeTideOptions(options?): ResolvedThemeTideOptions
|
|
169
|
-
themeTideDefaults, themeTideRanges
|
|
62
|
+
```tsx
|
|
63
|
+
tide.run(() => setTheme("dark"));
|
|
170
64
|
```
|
|
171
65
|
|
|
172
|
-
|
|
66
|
+
If your theme lives in React state, wrap the update in `flushSync` so the page changes before the water starts:
|
|
173
67
|
|
|
174
|
-
|
|
68
|
+
```tsx
|
|
69
|
+
import { flushSync } from "react-dom";
|
|
175
70
|
|
|
176
|
-
|
|
71
|
+
tide.run(() => flushSync(() => setTheme("dark")));
|
|
72
|
+
```
|
|
177
73
|
|
|
178
|
-
|
|
74
|
+
It works in Next.js too. Use the hook in a Client Component.
|
|
179
75
|
|
|
180
|
-
|
|
76
|
+
People who prefer reduced motion get an instant change. So do browsers without View Transitions.
|
|
181
77
|
|
|
182
|
-
|
|
183
|
-
- **Firefox 157:** animation timing and mask geometry checked through computed styles, at 1280 × 800 and 390 × 844. Firefox screenshots taken through Playwright don't include the transition layer, so frames haven't been checked by eye in automation yet.
|
|
184
|
-
- **Safari and WebKit:** not tested yet.
|
|
78
|
+
## Sites using it
|
|
185
79
|
|
|
186
|
-
|
|
80
|
+
| Mid-tide | Site |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| <a href="https://vertexx.fit"><img src="https://raw.githubusercontent.com/alawonn/react-theme-tide/main/docs/images/site-vertexx.png" width="320" alt="Vertexx switching from dark to light, paused partway up"></a> | [Vertexx](https://vertexx.fit) |
|
|
83
|
+
| <a href="https://meau.me"><img src="https://raw.githubusercontent.com/alawonn/react-theme-tide/main/docs/images/site-meau-me.png" width="320" alt="meau.me switching from light to dark, with long, slow waves"></a> | [meau.me](https://meau.me) |
|
|
84
|
+
| <a href="https://alawonn.com"><img src="https://raw.githubusercontent.com/alawonn/react-theme-tide/main/docs/images/site-alawonn.png" width="320" alt="alawonn.com switching from light to dark, with short, quick waves"></a> | [alawonn.com](https://alawonn.com) |
|
|
187
85
|
|
|
188
|
-
|
|
86
|
+
Using it on your site? [Open an issue](https://github.com/alawonn/react-theme-tide/issues/new?title=Add%20my%20site%20to%20the%20README&body=Site%20name%3A%0ASite%20URL%3A%0A) with the name and link, or send a pull request that adds it to this table.
|
|
189
87
|
|
|
190
|
-
|
|
191
|
-
npm install
|
|
192
|
-
npm run demo
|
|
193
|
-
```
|
|
88
|
+
## More
|
|
194
89
|
|
|
195
|
-
|
|
90
|
+
- [Usage and options](https://github.com/alawonn/react-theme-tide/blob/main/docs/usage.md): timing, waves, the API and Next.js setup.
|
|
91
|
+
- [How it works](https://github.com/alawonn/react-theme-tide/blob/main/docs/how-it-works.md): what happens on screen, edge cases and tested browsers.
|
|
92
|
+
- [Contributing](https://github.com/alawonn/react-theme-tide/blob/main/CONTRIBUTING.md)
|
|
196
93
|
|
|
197
94
|
## License
|
|
198
95
|
|