Skip to content

Commit 473b6fc

Browse files
authored
feat: synchronous mode change (#147)
1 parent 521926e commit 473b6fc

File tree

8 files changed

+82
-32
lines changed

8 files changed

+82
-32
lines changed

.changeset/mean-years-yell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"mode-watcher": minor
3+
---
4+
5+
feat: expose `synchronousModeChanges` prop to opt out of performance improvements for synchronous mode changes. Defaults to `false`.

docs/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
"check:watch": "pnpm build:content && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
1616
},
1717
"devDependencies": {
18-
"@svecodocs/kit": "^0.2.1",
18+
"@svecodocs/kit": "^0.3.0",
1919
"@sveltejs/adapter-cloudflare": "^4.8.0",
2020
"@sveltejs/kit": "^2.20.3",
2121
"@sveltejs/vite-plugin-svelte": "^5.0.3",
2222
"@tailwindcss/vite": "^4.1.1",
23-
"mdsx": "^0.0.6",
23+
"mdsx": "^0.0.7",
2424
"mode-watcher": "workspace:*",
2525
"phosphor-svelte": "^3.0.1",
2626
"svelte": "^5.27.0",

docs/src/content/components/mode-watcher.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,11 @@ The classes to add to the root `html` element when the mode is `'light'`.
153153
<PropField name="disableHeadScriptInjection" type="boolean" defaultValue="false">
154154
Whether to disable the injected script tag that sets the initial mode. Set this if you are manually injecting the script using a hook.
155155
</PropField>
156+
157+
<PropField name="synchronousModeChanges" type="boolean" defaultValue="false">
158+
159+
Whether to run the mode changes synchronously instead of using an animation frame. If true, will have an impact on blocking performance due to blocking the main thread.
160+
161+
Only applicable if `disableTransitions` is set to `true`.
162+
163+
</PropField>

packages/mode-watcher/src/lib/components/mode-watcher.svelte

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
disableTransitions,
99
lightClassNames,
1010
mode,
11+
synchronousModeChanges,
1112
theme,
1213
themeColors,
1314
} from "$lib/states.svelte.js";
@@ -28,6 +29,7 @@
2829
themeStorageKey: themeStorageKeyProp = "mode-watcher-theme",
2930
modeStorageKey: modeStorageKeyProp = "mode-watcher-mode",
3031
disableHeadScriptInjection = false,
32+
synchronousModeChanges: synchronousModeChangesProp = false,
3133
}: ModeWatcherProps = $props();
3234
3335
modeStorageKey.current = modeStorageKeyProp;
@@ -36,6 +38,11 @@
3638
lightClassNames.current = lightClassNamesProp;
3739
disableTransitions.current = disableTransitionsProp;
3840
themeColors.current = themeColorsProp;
41+
synchronousModeChanges.current = synchronousModeChangesProp;
42+
43+
$effect.pre(() => {
44+
synchronousModeChanges.current = synchronousModeChangesProp;
45+
});
3946
4047
$effect.pre(() => {
4148
disableTransitions.current = disableTransitionsProp;

packages/mode-watcher/src/lib/states.svelte.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ThemeColors } from "./types.js";
44
import { withoutTransition } from "./without-transition.js";
55
import { systemPrefersMode, userPrefersMode } from "./mode-states.svelte.js";
66
import { customTheme } from "./theme-state.svelte.js";
7+
import { untrack } from "svelte";
78

89
/**
910
* Theme colors for light and dark modes.
@@ -15,6 +16,13 @@ export const themeColors = box<ThemeColors>(undefined);
1516
*/
1617
export const disableTransitions = box(true);
1718

19+
/**
20+
* Whether to run the mode changes synchronously instead of using
21+
* an animation frame. If true, will have an impact on blocking performance
22+
* due to blocking the main thread.
23+
*/
24+
export const synchronousModeChanges = box(false);
25+
1826
/**
1927
* The classnames to add to the root `html` element when the mode is dark.
2028
*/
@@ -60,7 +68,7 @@ function createDerivedMode() {
6068
}
6169

6270
if (disableTransitions.current) {
63-
withoutTransition(update);
71+
withoutTransition(update, synchronousModeChanges.current);
6472
} else {
6573
update();
6674
}
@@ -86,7 +94,10 @@ function createDerivedTheme() {
8694
}
8795

8896
if (disableTransitions.current) {
89-
withoutTransition(update);
97+
withoutTransition(
98+
update,
99+
untrack(() => synchronousModeChanges.current)
100+
);
90101
} else {
91102
update();
92103
}

packages/mode-watcher/src/lib/types.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ export type ModeWatcherProps = {
88
* Whether to automatically track operating system preferences
99
* and update the mode accordingly.
1010
*
11-
* @defaultValue `true`
11+
* @default `true`
1212
*/
1313
track?: boolean;
1414

1515
/**
1616
* The default mode to use instead of the user's preference.
1717
*
18-
* @defaultValue `"system"`
18+
* @default `"system"`
1919
*/
2020
defaultMode?: Mode;
2121

@@ -28,7 +28,7 @@ export type ModeWatcherProps = {
2828
* <html data-theme="your-custom-theme"></html>
2929
* ```
3030
*
31-
* @defaultValue `undefined`
31+
* @default `undefined`
3232
*/
3333
defaultTheme?: string;
3434

@@ -45,44 +45,55 @@ export type ModeWatcherProps = {
4545
/**
4646
* The classname to add to the root `html` element when the mode is dark.
4747
*
48-
* @defaultValue `["dark"]`
48+
* @default `["dark"]`
4949
*/
5050
darkClassNames?: string[];
5151

5252
/**
5353
* The classname to add to the root `html` element when the mode is light.
5454
*
55-
* @defaultValue `[]`
55+
* @default `[]`
5656
*/
5757
lightClassNames?: string[];
5858

5959
/**
6060
* Optionally provide a custom local storage key to use for storing the mode.
6161
*
62-
* @defaultValue `'mode-watcher-mode'`
62+
* @default `'mode-watcher-mode'`
6363
*/
6464
modeStorageKey?: string;
6565

6666
/**
6767
* Optionally provide a custom local storage key to use for storing the theme.
6868
*
69-
* @defaultValue `'mode-watcher-theme'`
69+
* @default `'mode-watcher-theme'`
7070
*/
7171
themeStorageKey?: string;
7272

7373
/**
7474
* An optional nonce to use for the injected script tag to allow-list mode-watcher
7575
* if you are using a Content Security Policy.
7676
*
77-
* @defaultValue `undefined`
77+
* @default `undefined`
7878
*/
7979
nonce?: string;
8080

8181
/**
8282
* Whether to disable the injected script tag that sets the initial mode.
8383
* Set this if you are manually injecting the script using a hook.
8484
*
85-
* @defaultValue `false`
85+
* @default `false`
8686
*/
8787
disableHeadScriptInjection?: boolean;
88+
89+
/**
90+
* Whether to run the mode changes synchronously instead of using
91+
* an animation frame. If true, will have an impact on blocking performance
92+
* due to blocking the main thread.
93+
*
94+
* Only applicable if `disableTransitions` is set to `true`.
95+
*
96+
* @default `false`
97+
*/
98+
synchronousModeChanges?: boolean;
8899
};

packages/mode-watcher/src/lib/without-transition.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function getStyleElement() {
3030

3131
// Perform a task without any css transitions
3232
// eslint-disable-next-line @typescript-eslint/no-explicit-any
33-
export function withoutTransition(action: () => any) {
33+
export function withoutTransition(action: () => any, synchronous = false) {
3434
if (typeof document === "undefined") return;
3535

3636
// Skip transition disabling on initial load
@@ -62,22 +62,30 @@ export function withoutTransition(action: () => any) {
6262
}
6363
};
6464

65+
function executeAction() {
66+
action();
67+
// defer enable to ensure action completes
68+
window.requestAnimationFrame(enable);
69+
}
70+
6571
// Use requestAnimationFrame for better performance
6672
if (typeof window.requestAnimationFrame !== "undefined") {
6773
disable();
68-
// defer action to next frame to avoid blocking
69-
window.requestAnimationFrame(() => {
70-
action();
71-
// defer enable to ensure action completes
72-
window.requestAnimationFrame(enable);
73-
});
74+
if (synchronous) {
75+
executeAction();
76+
} else {
77+
// defer action to next frame to avoid blocking
78+
window.requestAnimationFrame(() => {
79+
executeAction();
80+
});
81+
}
7482
return;
7583
}
7684

7785
// Fallback for older browsers
7886
disable();
7987
timeoutAction = window.setTimeout(() => {
8088
action();
81-
timeoutEnable = window.setTimeout(enable, 16); // ~1 frame at 60fps
89+
timeoutEnable = window.setTimeout(enable, 16);
8290
}, 16);
8391
}

pnpm-lock.yaml

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)