diff --git a/packages/runed/src/lib/utilities/index.ts b/packages/runed/src/lib/utilities/index.ts index 55f528b8..c2ef8e13 100644 --- a/packages/runed/src/lib/utilities/index.ts +++ b/packages/runed/src/lib/utilities/index.ts @@ -25,6 +25,7 @@ export * from "./use-event-listener/index.js"; export * from "./use-geolocation/index.js"; export * from "./use-intersection-observer/index.js"; export * from "./use-mutation-observer/index.js"; +export * from "./use-preferred-color-scheme/index.js"; export * from "./use-resize-observer/index.js"; export * from "./use-interval/index.js"; export * from "./use-throttle/index.js"; diff --git a/packages/runed/src/lib/utilities/use-preferred-color-scheme/index.ts b/packages/runed/src/lib/utilities/use-preferred-color-scheme/index.ts new file mode 100644 index 00000000..693c395e --- /dev/null +++ b/packages/runed/src/lib/utilities/use-preferred-color-scheme/index.ts @@ -0,0 +1 @@ +export * from "./use-preferred-color-scheme.svelte.js"; diff --git a/packages/runed/src/lib/utilities/use-preferred-color-scheme/use-preferred-color-scheme.svelte.ts b/packages/runed/src/lib/utilities/use-preferred-color-scheme/use-preferred-color-scheme.svelte.ts new file mode 100644 index 00000000..757622c4 --- /dev/null +++ b/packages/runed/src/lib/utilities/use-preferred-color-scheme/use-preferred-color-scheme.svelte.ts @@ -0,0 +1,37 @@ +import { MediaQuery } from "svelte/reactivity"; + +export type ColorSchemeType = "dark" | "light" | "no-preference"; + +export type UsePreferredColorSchemeOptions = { + /** + * Fallback value for server-side rendering + * @defaultValue "no-preference" + */ + fallback?: ColorSchemeType; +}; + +/** + * Reactive prefers-color-scheme media query. + * + * @see https://runed.dev/docs/utilities/use-preferred-color-scheme + */ +export function usePreferredColorScheme(options?: UsePreferredColorSchemeOptions) { + const { fallback = "no-preference" } = options ?? {}; + + // Map ColorSchemeType fallback to boolean values for MediaQuery + const isLightFallback = fallback === "light"; + const isDarkFallback = fallback === "dark"; + + const isLight = new MediaQuery("(prefers-color-scheme: light)", isLightFallback); + const isDark = new MediaQuery("(prefers-color-scheme: dark)", isDarkFallback); + const current = $derived.by(() => { + if (isDark.current) return "dark"; + if (isLight.current) return "light"; + return "no-preference"; + }); + return { + get current() { + return current; + }, + }; +} diff --git a/packages/runed/src/lib/utilities/use-preferred-color-scheme/use-preferred-color-scheme.test.svelte.ts b/packages/runed/src/lib/utilities/use-preferred-color-scheme/use-preferred-color-scheme.test.svelte.ts new file mode 100644 index 00000000..fa2e91c2 --- /dev/null +++ b/packages/runed/src/lib/utilities/use-preferred-color-scheme/use-preferred-color-scheme.test.svelte.ts @@ -0,0 +1,35 @@ +import { describe, expect } from "vitest"; +import { flushSync } from "svelte"; +import { usePreferredColorScheme } from "./use-preferred-color-scheme.svelte.js"; +import { testWithEffect } from "$lib/test/util.svelte.js"; + +describe("usePreferredColorScheme", () => { + testWithEffect("initializes with dark fallback", () => { + const util = usePreferredColorScheme({ fallback: "dark" }); + flushSync(); + expect(typeof util.current).toBe("string"); + expect(["dark", "light", "no-preference"].includes(util.current)).toBe(true); + }); + + testWithEffect("initializes with light fallback", () => { + const util = usePreferredColorScheme({ fallback: "light" }); + flushSync(); + expect(typeof util.current).toBe("string"); + expect(["dark", "light", "no-preference"].includes(util.current)).toBe(true); + }); + + testWithEffect("initializes with no-preference fallback (default)", () => { + const util = usePreferredColorScheme(); + flushSync(); + expect(typeof util.current).toBe("string"); + expect(["dark", "light", "no-preference"].includes(util.current)).toBe(true); + }); + + testWithEffect("returns object with current getter", () => { + const util = usePreferredColorScheme({ fallback: "no-preference" }); + flushSync(); + expect(typeof util).toBe("object"); + expect(typeof util.current).toBe("string"); + expect(["dark", "light", "no-preference"].includes(util.current)).toBe(true); + }); +}); diff --git a/sites/docs/src/content/utilities/use-preferred-color-scheme.md b/sites/docs/src/content/utilities/use-preferred-color-scheme.md new file mode 100644 index 00000000..aa7557ce --- /dev/null +++ b/sites/docs/src/content/utilities/use-preferred-color-scheme.md @@ -0,0 +1,73 @@ +--- +title: usePreferredColorScheme +description: Detect color scheme preference using the browser's prefers-color-scheme media query. +category: Sensors +--- + + + +`usePreferredColorScheme` provides a reactive string that reflects the user's color scheme +preference based on their browser or OS settings. It uses the +[prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) +media query and updates automatically when the preference changes. + +## Demo + + + +## Usage + +```svelte + + +
+ {colorScheme.current === "dark" + ? "🌙 Dark mode" + : colorScheme.current === "light" + ? "☀️ Light mode" + : "🎨 No preference"} +
+ + +
+ {colorSchemeWithDarkFallback.current === "dark" ? "🌙 Dark mode (fallback)" : "☀️ Light mode"} +
+ +
+ {colorSchemeWithLightFallback.current === "light" ? "☀️ Light mode (fallback)" : "🌙 Dark mode"} +
+``` + +## Type Definition + +```ts +type ColorSchemeType = "dark" | "light" | "no-preference"; + +type UsePreferredColorSchemeOptions = { + /** + * Fallback value for server-side rendering + * @defaultValue "no-preference" + */ + fallback?: ColorSchemeType; +}; + +function usePreferredColorScheme(options?: UsePreferredColorSchemeOptions): { + readonly current: ColorSchemeType; +}; +``` + +## Notes + +- Uses the `prefers-color-scheme: dark` and `prefers-color-scheme: light` media queries. +- Returns "dark", "light", or "no-preference" based on user's system preference. +- During server-side rendering, returns the specified fallback value (defaults to "no-preference"). +- Automatically updates when user changes their system color scheme preference. +- Works with both light and dark theme preferences and detects when no preference is set. diff --git a/sites/docs/src/lib/components/demos/use-preferred-color-scheme.svelte b/sites/docs/src/lib/components/demos/use-preferred-color-scheme.svelte new file mode 100644 index 00000000..9b340282 --- /dev/null +++ b/sites/docs/src/lib/components/demos/use-preferred-color-scheme.svelte @@ -0,0 +1,11 @@ + + + + Preferred Color Scheme: + {{ colorScheme: colorScheme.current }} +