diff --git a/components/Captcha/Captcha.tsx b/components/Captcha/Captcha.tsx
index 8dae73b2..da0013a7 100644
--- a/components/Captcha/Captcha.tsx
+++ b/components/Captcha/Captcha.tsx
@@ -22,7 +22,7 @@ export type CaptchaProps = {
const Captcha = ({ size = "normal", theme, className, ...rest }: CaptchaProps) => {
const hasMounted = useHasMounted();
- const { resolvedTheme } = useTheme();
+ const { activeTheme } = useTheme();
return (
@@ -32,7 +32,7 @@ const Captcha = ({ size = "normal", theme, className, ...rest }: CaptchaProps) =
reCaptchaCompat={false}
tabIndex={0}
size={size}
- theme={theme || (resolvedTheme === "dark" ? "dark" : "light")}
+ theme={theme || (activeTheme === "dark" ? "dark" : "light")}
{...rest}
/>
)}
diff --git a/components/Comments/Comments.tsx b/components/Comments/Comments.tsx
index 357bf303..b4c37805 100644
--- a/components/Comments/Comments.tsx
+++ b/components/Comments/Comments.tsx
@@ -18,7 +18,7 @@ export type CommentsProps = ComponentProps
& {
};
const Comments = ({ title, ...rest }: CommentsProps) => {
- const { resolvedTheme } = useTheme();
+ const { activeTheme } = useTheme();
// TODO: use custom `` spinner component during suspense
return (
@@ -29,7 +29,7 @@ const Comments = ({ title, ...rest }: CommentsProps) => {
mapping="specific"
reactionsEnabled="1"
emitMetadata="0"
- theme={resolvedTheme === "dark" ? "dark" : "light"}
+ theme={activeTheme === "dark" ? "dark" : "light"}
/>
);
diff --git a/components/Layout/Layout.tsx b/components/Layout/Layout.tsx
index 9e6f48c2..23cc46e0 100644
--- a/components/Layout/Layout.tsx
+++ b/components/Layout/Layout.tsx
@@ -33,13 +33,13 @@ export type LayoutProps = ComponentProps & {
};
const Layout = ({ container = true, children, ...rest }: LayoutProps) => {
- const { resolvedTheme } = useTheme();
+ const { activeTheme } = useTheme();
return (
<>
{/* dynamically set browser theme color to match the background color; default to light for SSR */}
-
+
diff --git a/components/ThemeScript/ThemeScript.tsx b/components/ThemeScript/ThemeScript.tsx
index 0eddbf71..a38c6236 100644
--- a/components/ThemeScript/ThemeScript.tsx
+++ b/components/ThemeScript/ThemeScript.tsx
@@ -1,13 +1,13 @@
import { useMemo } from "react";
import { minify } from "uglify-js";
import { clientScript } from "./client";
-import { darkModeQuery, themeStorageKey, themeClassNames } from "../../lib/config/themes";
+import { themeClassNames, themeStorageKey } from "../../lib/config/themes";
const ThemeScript = () => {
const minified = useMemo(() => {
// since the client function will end up being injected as a plain dumb string, we need to set dynamic values here:
const functionString = String(clientScript)
- .replace('"__MEDIA_QUERY__"', `"${darkModeQuery}"`)
+ .replace('"__MEDIA_QUERY__"', `"(prefers-color-scheme: dark)"`)
.replace('"__STORAGE_KEY__"', `"${themeStorageKey}"`)
.replace('"__CLASS_NAMES__"', JSON.stringify(themeClassNames));
diff --git a/components/ThemeToggle/ThemeToggle.tsx b/components/ThemeToggle/ThemeToggle.tsx
index 138e9590..57576a05 100644
--- a/components/ThemeToggle/ThemeToggle.tsx
+++ b/components/ThemeToggle/ThemeToggle.tsx
@@ -1,4 +1,4 @@
-import { useEffect, memo } from "react";
+import { useEffect, useId, memo } from "react";
import { useMedia } from "react-use";
import { useSpring, animated, Globals } from "@react-spring/web";
import { useTheme } from "../../hooks/use-theme";
@@ -19,17 +19,17 @@ const Button = styled("button", {
});
export type ThemeToggleProps = {
- id?: string;
className?: string;
};
-const ThemeToggle = ({ id = "nav", className }: ThemeToggleProps) => {
+const ThemeToggle = ({ className }: ThemeToggleProps) => {
const hasMounted = useHasMounted();
+ const { activeTheme, setTheme } = useTheme();
const prefersReducedMotion = useMedia("(prefers-reduced-motion: reduce)", false);
- const { resolvedTheme, setTheme } = useTheme();
+ const maskId = useId(); // SSR-safe ID to cross-reference areas of the SVG
- // default to light since `resolvedTheme` might be undefined
- const safeTheme = resolvedTheme === "dark" ? "dark" : "light";
+ // default to light since `activeTheme` might be undefined
+ const safeTheme = activeTheme === "dark" ? "dark" : "light";
// accessibility: skip animation if user prefers reduced motion
useEffect(() => {
@@ -120,7 +120,7 @@ const ThemeToggle = ({ id = "nav", className }: ThemeToggleProps) => {
}}
className={className}
>
-
+
{
cx="12"
cy="12"
fill="currentColor"
- mask={`url(#moon-mask-${id})`}
+ mask={`url(#mask-${maskId})`}
// @ts-ignore
style={centerCircleProps}
/>
diff --git a/components/TweetEmbed/TweetEmbed.tsx b/components/TweetEmbed/TweetEmbed.tsx
index 6ffcc9c3..535f06fd 100644
--- a/components/TweetEmbed/TweetEmbed.tsx
+++ b/components/TweetEmbed/TweetEmbed.tsx
@@ -8,7 +8,7 @@ export type TweetEmbedProps = {
};
const TweetEmbed = ({ id, options }: TweetEmbedProps) => {
- const { resolvedTheme } = useTheme();
+ const { activeTheme } = useTheme();
return (
{
options={{
dnt: true,
align: "center",
- theme: resolvedTheme === "dark" ? "dark" : "light",
+ theme: activeTheme === "dark" ? "dark" : "light",
...options,
}}
/>
diff --git a/contexts/ThemeContext.tsx b/contexts/ThemeContext.tsx
index 20f2711f..493eed1d 100644
--- a/contexts/ThemeContext.tsx
+++ b/contexts/ThemeContext.tsx
@@ -1,20 +1,16 @@
import { createContext, useCallback, useEffect, useState } from "react";
import { useLocalStorage, useMedia } from "react-use";
-import { darkModeQuery, themeStorageKey } from "../lib/config/themes";
+import { themeStorageKey } from "../lib/config/themes";
import type { Context, PropsWithChildren } from "react";
export const ThemeContext: Context<{
- /** Update the theme manually. */
- setTheme?: (theme: string) => void;
- /** The user's website theme setting ("light" or "dark", or undefined if unset). */
- preferredTheme?: string;
/**
- * If the theme setting is undefined, this returns whether the system preference resolved to "light" or "dark". If the
- * preference is set, the value is identical to `preferredTheme`.
- *
- * Note to self: you probably want this.
+ * If the user's theme preference is unset, this returns whether the system preference resolved to "light" or "dark".
+ * If the user's theme preference is set, the preference is returned instead, regardless of their system's theme.
*/
- resolvedTheme?: string;
+ activeTheme?: "light" | "dark";
+ /** Update the theme manually and save to local storage. */
+ setTheme?: (theme: string) => void;
}> = createContext({});
// provider used once in _app.tsx to wrap entire app
@@ -27,12 +23,13 @@ export const ThemeProvider = ({
[themeName: string]: string;
};
}>) => {
- // keep track of if/when the user has set their theme *here*:
+ // keep track of if/when the user has set their theme *on this site*
const [preferredTheme, setPreferredTheme] = useLocalStorage(themeStorageKey, null, { raw: true });
- // save the end result no matter how we got there (by preference or by system):
- const [resolvedTheme, setResolvedTheme] = useState("");
+ // keep track of changes to the user's OS/browser dark mode setting
+ const [systemTheme, setSystemTheme] = useState("");
// hook into system `prefers-dark-mode` setting
- const isSystemDark = useMedia(darkModeQuery, false);
+ // https://web.dev/prefers-color-scheme/#the-prefers-color-scheme-media-query
+ const isSystemDark = useMedia("(prefers-color-scheme: dark)", false);
// get the theme names (light, dark) via passed-in classnames' keys
const themeNames = Object.keys(classNames);
@@ -54,28 +51,31 @@ export const ThemeProvider = ({
// listen for changes in OS preference
useEffect(() => {
- const systemTheme = isSystemDark ? "dark" : "light";
+ // translate boolean to theme string
+ const systemResolved = isSystemDark ? "dark" : "light";
- // keep track of the resolved theme whether or not we change it below
- setResolvedTheme(systemTheme);
+ // keep track of the system theme whether or not we override it manually
+ setSystemTheme(systemResolved);
// only actually change the theme if preference is unset (and *don't* save new theme to storage)
if (!preferredTheme || !themeNames.includes(preferredTheme)) {
- changeTheme(systemTheme, false);
+ changeTheme(systemResolved, false);
}
}, [changeTheme, themeNames, preferredTheme, isSystemDark]);
// color-scheme handling (tells browser how to render built-in elements like forms, scrollbars, etc.)
useEffect(() => {
// only "light" and "dark" are valid here
- const colorScheme = ["light", "dark"].includes(preferredTheme) ? preferredTheme : resolvedTheme;
+ // https://web.dev/color-scheme/#the-color-scheme-css-property
+ const colorScheme = ["light", "dark"].includes(preferredTheme) ? preferredTheme : systemTheme;
document.documentElement.style?.setProperty("color-scheme", colorScheme);
- }, [preferredTheme, resolvedTheme]);
+ }, [preferredTheme, systemTheme]);
return (
{
// force save to local storage
@@ -83,8 +83,6 @@ export const ThemeProvider = ({
},
[changeTheme]
),
- preferredTheme: themeNames.includes(preferredTheme) ? preferredTheme : undefined,
- resolvedTheme: themeNames.includes(preferredTheme) ? preferredTheme : resolvedTheme,
}}
>
{children}
diff --git a/lib/config/themes.ts b/lib/config/themes.ts
index 265e3f01..2f6ff238 100644
--- a/lib/config/themes.ts
+++ b/lib/config/themes.ts
@@ -12,8 +12,5 @@ export const themeColors = {
dark: darkTheme.colors.backgroundOuter?.value,
};
-// https://web.dev/prefers-color-scheme/#the-prefers-color-scheme-media-query
-export const darkModeQuery = "(prefers-color-scheme: dark)";
-
// local storage key
export const themeStorageKey = "preferred-theme";