发布于 2026-01-06 0 阅读
0

使用 Hooks、媒体查询和 CSS 变量为你的 React 应用添加暗黑模式

使用 Hooks、媒体查询和 CSS 变量为你的 React 应用添加暗黑模式

深色模式正迅速成为网络上的一项基本功能——Twitter 最近的重新设计已经内置了这项备受期待的功能,Facebook 的(测试版)重新设计也是如此,更不用说许多较小的网站也添加了支持。

为了跟上潮流,我决定尝试给自己的个人网站添加深色模式。花了一晚上挑选颜色,又拖延了半天技术方案,结果发现实际操作起来比我想象的要快得多也容易得多。我在这里详细介绍了我的方法,希望能对其他人有所帮助!

注意:这种方法非常适合小型网站,但对于更复杂的情况,您可能需要将其与其他技术结合起来——文末链接了一些可能有用的资源。

那么,我们究竟想要构建什么呢?

问得好。我将重点介绍以下几个特点:

  • 检测设备是否在系统级别设置为深色模式
  • 系统级设置更改时切换主题
  • 一个简单的系统(使用 CSS 变量)用于切换整个网站的颜色。
  • 一个开关,允许用户手动切换深色和浅色主题。
  • 一个 SCSS mixin,用于支持更复杂的主题,当您需要做的不仅仅是替换颜色时。

这里有一个简单的例子来说明它的样子——如果您时间紧迫,可以直接查看代码,了解各个部分是如何组合在一起的:

使用媒体查询检测深色模式

首先,我们用一些 CSS 代码来检测用户何时将设备设置为深色模式。为此,我们将使用媒体查询

CSS媒体查询最常见的用途是根据浏览器窗口大小来改变样式。但最近,它们的功能变得更加强大,许多可检测的特性已被纳入最新的规范中。

我们关注的媒体查询功能是 `color-special` prefers-color-scheme。顾名思义,它可以检测用户偏好的配色方案——`color-special` dark、` color- lightspecial` 或no-preference`color-special`。它的用法如下:

    @media (prefers-color-scheme: dark) {
      /* dark theme styles go here */
    }
Enter fullscreen mode Exit fullscreen mode

媒体查询中的所有样式仅在用户系统设置为深色模式时才会生效。仅凭这一点就足以让你开始为网站添加深色主题了!以下是一个简单组件的纯 CSS 示例:

    .TextCard {
      background: white;
      color: black;

      margin: 0;
      padding: 10px 20px;
      border-radius: 20px;
    }

    @media (prefers-color-scheme: dark) {
      .TextCard {
        background: black;
        color: white;
      }
    }
Enter fullscreen mode Exit fullscreen mode

为了简单起见,这里我使用了诸如“黑色”和“白色”之类的颜色名称。在实际实现中,我会将常用颜色提取到 SCSS 变量中,以保持一致性。

太棒了!这是个不错的进展。但是,在对几个组件进行此操作后,您可能会注意到很多重复工作:您很可能会反复切换相同的颜色。例如,如果您的大部分文本都是某种深灰色,那么您很可能需要在所有使用该颜色的地方添加相同的媒体查询,以便在深色模式下将其替换为(不同的)特定色调。

接下来,我们需要了解拼图的下一部分:CSS 变量

使用 CSS 变量交换颜色

使用 CSS 变量,我们可以集中定义默认(浅色模式)颜色,然后在启用深色模式时切换到不同的颜色。如果您熟悉 SCSS 变量,那么它们与之类似,区别在于我们可以在运行时动态更改它们的值——这对于将它们用作主题系统的一部分至关重要。

举个简单的例子,我们可以将 ` primaryTextColora` 和 ` b` 定义primaryBackgroundColor为变量。对于默认的浅色主题,我们可以这样设置它们:

    html {
      --primaryBackgroundColor: white;
      --primaryTextColor: black;
    }
Enter fullscreen mode Exit fullscreen mode

在 html 元素上设置变量意味着页面上的其他所有内容都可以访问这些变量,因为所有内容都将是 html 元素的后代。

要使用这些变量,我们需要将所有样式中相关的硬编码颜色替换为相应的var()值:

    .TextCard {
-      background: white;
+      background: var(--primaryBackgroundColor);
-      color: black;
+      color: var(--primaryTextColor);

      margin: 0;
      padding: 10px 20px;
      border-radius: 20px;
    }
Enter fullscreen mode Exit fullscreen mode

现在我们需要让变量的值在深色模式激活时发生变化。为此,我们可以使用之前提到的查询选择器,但这次我们不用将其应用于每个单独的组件,而是只使用一次,将其定位到 HTML 元素:

    html {
      --primaryBackgroundColor: white;
      --primaryTextColor: black;
    }
+
+    @media (prefers-color-scheme: dark) {
+      html {
+        --primaryBackgroundColor: black;
+        --primaryTextColor: white;
+      }
+    }
Enter fullscreen mode Exit fullscreen mode

请注意,查询选择器中两个变量的值已互换。启用深色模式后,此更改将传播到所有使用这些变量的地方,并立即切换这些元素的颜色。

将此功能扩展到网站的其他区域非常简单,只需定义新变量,在暗黑模式媒体查询中将它们设置为不同的值,然后将代码中硬编码的颜色值替换为变量即可。

以下是对这种方法的一个简要演示:

添加一个覆盖按钮来切换主题

目前,我们已经构建了一种相当易于管理且极其轻量级的方案,可以尊重用户的系统颜色偏好。但是,如果我们想让用户拥有更多控制权,让他们手动选择主题呢?也许他们的设备不支持系统级深色模式,或者他们希望除了我们的网站之外的所有内容都使用深色模式。

为此,我们将添加一个切换按钮,该按钮不仅允许手动切换主题,而且还会自动反映系统级别的偏好。

我选择使用react-toggle库来实现实际的切换按钮,但这应该适用于任何切换组件——无论是来自库、自定义组件,甚至是可靠的<checkbox>元素。

这是我最初使用的代码:

    import React from "react";
    import Toggle from "react-toggle";

    export const DarkToggle = () => {
      return (
        <Toggle
          className="DarkToggle"
          icons={{ checked: "🌙", unchecked: "🔆" }}
          aria-label="Dark mode"
        />
      );
    };
Enter fullscreen mode Exit fullscreen mode

我们首先添加一些状态来控制开关是否设置为深色模式,并将其连接到开关:

+   import React, { useState } from "react";
+   import Toggle from "react-toggle";

    export const DarkToggle = () => {
+     const [isDark, setIsDark] = useState(true);

      return (
        <Toggle
          className="DarkToggle"
+         checked={isDark}
+         onChange={event => setIsDark(event.target.checked)}
          icons={{ checked: "🌙", unchecked: "🔆" }}
          aria-label="Dark mode"
        />
      );
    };
Enter fullscreen mode Exit fullscreen mode

如果你还不熟悉 React 的useStatehook,那么绝对值得看看官方的 hook 文档

如果你打开 React 开发工具,应该可以看到isDark点击切换按钮后状态会更新:

点击切换按钮会导致 React 开发工具中显示的状态在“true”和“false”之间切换。

现在,让我们添加一些基于标准的技巧,使切换开关能够自动匹配用户的系统暗黑模式设置。为此,我们将使用一个名为react-responsive 的出色 React 库。它允许你获取 CSS 媒体查询的结果,并在查询结果更改时自动更新值。这非常实用,它完全基于标准的 JavaScript matchMedia 函数构建。

正如你可能已经猜到的,我们将使用的媒体查询是 `<media query="image.h>` prefers-color-scheme: dark。它的代码如下所示:

    import React, { useState } from "react";
    import Toggle from "react-toggle";
    import { useMediaQuery } from "react-responsive";

    export const DarkToggle = () => {
      const systemPrefersDark = useMediaQuery(
        {
          query: "(prefers-color-scheme: dark)"
        },
        undefined,
        prefersDark => {
          setIsDark(prefersDark);
        }
      );

      const [isDark, setIsDark] = useState(systemPrefersDark);
    };
Enter fullscreen mode Exit fullscreen mode

这个useMediaQuery钩子函数需要两个重要参数:媒体查询(第一个参数)和一个函数(第三个参数),当媒体查询结果发生变化时,该函数会被调用。我们希望isDark在媒体查询结果发生变化时更新状态,所以这段代码正是这样做的。

现在,如果您打开和关闭系统深色模式,该开关应该会同时自动切换。太棒了!

……但它还没有与我们的 CSS 连接,所以这个切换开关几乎没什么用。为了解决这个问题,我们需要在isDark状态改变时运行一些代码。React 的useEffect hook非常适合这项工作——我们给它一个函数,告诉它它依赖哪些属性(isDark在本例中),然后 React 会在属性改变时自动调用该函数:

    [...]

      const [isDark, setIsDark] = useState(systemPrefersDark);

      useEffect(() => {
        // whatever we put here will run whenever `isDark` changes
      }, [isDark]);

    [...]
Enter fullscreen mode Exit fullscreen mode

解决另一个难题需要对 CSS 稍作调整。我们的代码无法更改 `<color>` 的值prefers-color-scheme,这使得在当前设置下很难强制启用暗黑模式。因此,我们将让颜色变量在 HTML 元素具有dark`<class>` 时发生变化(我们稍后会动态地将该类添加到元素中):

    html {
      --primaryBackgroundColor: white;
      --primaryTextColor: black;
    }

    html.dark {
      --primaryBackgroundColor: black;
      --primaryTextColor: white;
    }
Enter fullscreen mode Exit fullscreen mode

最后,让我们更新函数体,根据条件是否为真useEffect来添加(和删除)类darkisDark

    import React, { useEffect, useState } from "react";
    import { useMediaQuery } from "react-responsive";
    import Toggle from "react-toggle";
+
+   const DARK_CLASS = "dark";

    export const DarkToggle = () => {
      const systemPrefersDark = useMediaQuery(
        {
          query: "(prefers-color-scheme: dark)"
        },
        undefined,
        prefersDark => {
          setIsDark(prefersDark);
        }
      );

      const [isDark, setIsDark] = useState(systemPrefersDark);
+
+     useEffect(() => {
+       if (isDark) {
+         document.documentElement.classList.add(DARK_CLASS)
+       } else {
+         document.documentElement.classList.remove(DARK_CLASS)
+       }
+     }, [isDark]);

      return (
        <Toggle
          className="DarkToggle"
          checked={isDark}
          onChange={event => setIsDark(event.target.checked)}
          icons={{ checked: "🌙", unchecked: "🔆" }}
          aria-label="Dark mode"
        />
      );
    };

Enter fullscreen mode Exit fullscreen mode

🎉 大功告成!现在,无论直接点击开关还是更改系统的深色模式设置,页面主题都会自动更改。

可选的收尾工作

处理更复杂的样式

我发现 CSS 变量功能强大,几乎可以满足我网站上所有需要的调整。然而,它们仍然无法处理一些特殊情况(或者处理起来不太方便),例如添加一个细微的边框,或者稍微调整阴影的不透明度,使其在深色模式下显示得更好。

针对这些情况,我创建了一个 SCSS mixin,它仅在启用深色模式时应用样式(类似于我们引入变量之前的做法,即直接在每个组件的 CSS 中使用媒体查询)。它的用法如下:

    .Card {
      background: var(--backgroundPrimary);
      box-shadow: 0 4px 20px rgba(darken($mint, 15%), 0.22);

      @include whenDark {
         // styles to apply to the element when dark mode is active
         box-shadow: 0 4px 20px rgba(#000, 0.5);
      }
    }
Enter fullscreen mode Exit fullscreen mode

mixin 本身的代码使用了&SCSS 特性来引用调用 mixin 的选择器,并@content允许将内容传递给它:

    @mixin whenDark {
      html.dark & {
        @content;
      }
    }
Enter fullscreen mode Exit fullscreen mode

(如果你深入查看沙盒代码,你会发现我还使用了 mixin 来设置颜色变量,因此所有 CSS 都使用相同的代码来确定深色模式是否处于活动状态)。

支持不使用 JavaScript 的用户

由于我们将 CSS 从使用prefers-color-scheme媒体查询改为依赖 JavaScript 代码中设置的类,我们无意中破坏了禁用 JavaScript 的用户的暗黑模式支持。(如果您没有预渲染网站,则不会出现此问题,因为对于禁用 JavaScript 的用户,网站可能根本不会显示。)

幸运的是,如果您使用的是上面的 mixin,那么恢复支持就相当简单——只需更新它,使其在媒体查询处于活动状态时也应用任何样式即可:

    @mixin whenDark {
      html.dark & {
        @content;
      }

      @media (prefers-color-scheme: dark) {
        & {
          @content;
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode

更多提示和资源

模拟深色模式

Chrome 的开发者工具允许您通过渲染选项卡模拟 prefers-color-scheme 值。

如果你使用的是 Mac,Safari 的开发者工具也允许你一键切换到深色模式:

屏幕录像显示,在 Safari 开发者工具的“元素”面板中,点击了“深色模式”切换按钮。

记住用户的偏好

我还没试过这种方法,但它绝对值得探索。如果你的网站还没有数据持久化解决方案,那么 ` use-persisted-state`这个钩子非常适合用于暗黑模式切换。

从切换按钮外部查询深色模式

在我描述的这种设置中,唯一真正知道深色模式是否激活的组件是切换组件。对于简单的场景来说,这完全没问题,但如果其他 JavaScript 代码需要根据主题做出不同的行为呢?虽然我目前还没有遇到这种情况,但任何常用的状态共享方案都应该可以解决这个问题——无论是Context API、Redux 还是你网站已经在使用的其他任何技术。

浏览器支持

坏消息:我们的老朋友 Internet Explorer 不支持 CSS 变量。这意味着这种方法在 IE 上效果不佳——所有变量属性都会回退到它们的默认值/继承值(例如,文本颜色很可能是黑色)。如果您确实需要支持 IE,有几种选择——主要的选择是css-vars-ponyfill基于 SASS 的回退方案

资源

以下是一些您可能会觉得有用的其他资源(我确实觉得很有用):

哇哦,真是一段精彩的旅程!感谢你一路陪伴我走到这里,如果你觉得这些内容对你有所帮助,我很想看看你的作品!

文章来源:https://dev.to/nw/adding-dark-mode-to-your-react-app-with-hooks-media-queries-and-css-variables-50h0