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.
Files changed (2) hide show
  1. package/README.md +42 -145
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,12 +1,16 @@
1
1
  # React Theme Tide
2
2
 
3
- Change your site's theme with a water fill that rises from the bottom of the screen. It stops at 60% for one second while the waves keep moving, then fills the rest of the screen.
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
- What shows under the water is your real page in its new theme: text, backgrounds, borders and controls. It isn't a colored overlay. The reveal uses the browser's View Transitions API and a CSS mask, so it needs no screenshot library, no copy of your DOM and no React re-render on each frame.
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
- React Theme Tide doesn't store your theme. You keep your own theme state and storage, and the library animates around the function that applies it.
11
+ **[Try the demo](https://alawonn.github.io/react-theme-tide/)**
8
12
 
9
- > Status: first release (0.1). The API may still change before 1.0.
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
- React 18 or newer is a peer dependency. There's no stylesheet to import. While a tide runs, the library adds a small `<style>` element to `<head>` and removes it when the tide ends.
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
- Without React, call `themeTide` directly:
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
- ## Wrap a theme setter you already have
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
- Your storage key, your pre-paint script and your other controls keep working as before. Every control can use its own `useThemeTide()`; they all see the same `running` state.
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
- This applies to theme libraries that keep state in React context, too.
37
+ You need React 18 or newer. There's no CSS to import.
83
38
 
84
- ## Next.js App Router
39
+ ## Use it
85
40
 
86
- Use the hook in a Client Component (a file that starts with `"use client"`). Only the hook's module is marked `"use client"`. The rest of the package doesn't touch `window` or `document` when it's imported, so Server Components can import the defaults and helpers. On the server, `supportsThemeTide()` returns `false`.
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 { run, running } = useThemeTide();
48
+ const tide = useThemeTide();
95
49
  return (
96
50
  <button
97
51
  type="button"
98
- disabled={running}
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
- ```tsx
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
- export default function RootLayout({ children }: { children: React.ReactNode }) {
112
- return (
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
- ## Working with other View Transitions
66
+ If your theme lives in React state, wrap the update in `flushSync` so the page changes before the water starts:
173
67
 
174
- The tide animates the page's root snapshot (`::view-transition-new(root)`). If your page gives other elements their own `view-transition-name`, those elements cross-fade on their own during a tide and aren't covered by the water. Starting another View Transition while a tide runs ends the tide early; the theme still changes.
68
+ ```tsx
69
+ import { flushSync } from "react-dom";
175
70
 
176
- ## Browser support
71
+ tide.run(() => flushSync(() => setTheme("dark")));
72
+ ```
177
73
 
178
- The animation needs `document.startViewTransition`, `Element.animate` and CSS `mask-image`. Other browsers get the instant fallback.
74
+ It works in Next.js too. Use the hook in a Client Component.
179
75
 
180
- Tested so far, through Playwright automation on Linux:
76
+ People who prefer reduced motion get an instant change. So do browsers without View Transitions.
181
77
 
182
- - **Chromium 156:** animation checked frame by frame from screenshots, at 1280 × 800 and in Pixel 7 emulation (412 × 839, device pixel ratio 2.625).
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
- Known limitation: on screens with a fractional device pixel ratio, Chromium sometimes leaves the top-right corner device pixel in the old theme during the last frames.
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
- ## Demo
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
- ```bash
191
- npm install
192
- npm run demo
193
- ```
88
+ ## More
194
89
 
195
- Then open http://localhost:5179. The demo has light and dark controls, sliders for every option, long scrolling content and fixed elements.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-theme-tide",
3
- "version": "0.2.1",
3
+ "version": "0.2.5",
4
4
  "description": "Theme changes that rise up the page like water, using native View Transitions.",
5
5
  "license": "MIT",
6
6
  "type": "module",