使用 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 */
}
媒体查询中的所有样式仅在用户系统设置为深色模式时才会生效。仅凭这一点就足以让你开始为网站添加深色主题了!以下是一个简单组件的纯 CSS 示例:
.TextCard {
background: white;
color: black;
margin: 0;
padding: 10px 20px;
border-radius: 20px;
}
@media (prefers-color-scheme: dark) {
.TextCard {
background: black;
color: white;
}
}
为了简单起见,这里我使用了诸如“黑色”和“白色”之类的颜色名称。在实际实现中,我会将常用颜色提取到 SCSS 变量中,以保持一致性。
太棒了!这是个不错的进展。但是,在对几个组件进行此操作后,您可能会注意到很多重复工作:您很可能会反复切换相同的颜色。例如,如果您的大部分文本都是某种深灰色,那么您很可能需要在所有使用该颜色的地方添加相同的媒体查询,以便在深色模式下将其替换为(不同的)特定色调。
接下来,我们需要了解拼图的下一部分:CSS 变量✨
使用 CSS 变量交换颜色
使用 CSS 变量,我们可以集中定义默认(浅色模式)颜色,然后在启用深色模式时切换到不同的颜色。如果您熟悉 SCSS 变量,那么它们与之类似,区别在于我们可以在运行时动态更改它们的值——这对于将它们用作主题系统的一部分至关重要。
举个简单的例子,我们可以将 ` primaryTextColora` 和 ` b` 定义primaryBackgroundColor为变量。对于默认的浅色主题,我们可以这样设置它们:
html {
--primaryBackgroundColor: white;
--primaryTextColor: black;
}
在 html 元素上设置变量意味着页面上的其他所有内容都可以访问这些变量,因为所有内容都将是 html 元素的后代。
要使用这些变量,我们需要将所有样式中相关的硬编码颜色替换为相应的var()值:
.TextCard {
- background: white;
+ background: var(--primaryBackgroundColor);
- color: black;
+ color: var(--primaryTextColor);
margin: 0;
padding: 10px 20px;
border-radius: 20px;
}
现在我们需要让变量的值在深色模式激活时发生变化。为此,我们可以使用之前提到的查询选择器,但这次我们不用将其应用于每个单独的组件,而是只使用一次,将其定位到 HTML 元素:
html {
--primaryBackgroundColor: white;
--primaryTextColor: black;
}
+
+ @media (prefers-color-scheme: dark) {
+ html {
+ --primaryBackgroundColor: black;
+ --primaryTextColor: white;
+ }
+ }
请注意,查询选择器中两个变量的值已互换。启用深色模式后,此更改将传播到所有使用这些变量的地方,并立即切换这些元素的颜色。
将此功能扩展到网站的其他区域非常简单,只需定义新变量,在暗黑模式媒体查询中将它们设置为不同的值,然后将代码中硬编码的颜色值替换为变量即可。
以下是对这种方法的一个简要演示:
添加一个覆盖按钮来切换主题
目前,我们已经构建了一种相当易于管理且极其轻量级的方案,可以尊重用户的系统颜色偏好。但是,如果我们想让用户拥有更多控制权,让他们手动选择主题呢?也许他们的设备不支持系统级深色模式,或者他们希望除了我们的网站之外的所有内容都使用深色模式。
为此,我们将添加一个切换按钮,该按钮不仅允许手动切换主题,而且还会自动反映系统级别的偏好。
我选择使用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"
/>
);
};
我们首先添加一些状态来控制开关是否设置为深色模式,并将其连接到开关:
+ 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"
/>
);
};
如果你还不熟悉 React 的useStatehook,那么绝对值得看看官方的 hook 文档。
如果你打开 React 开发工具,应该可以看到isDark点击切换按钮后状态会更新:
现在,让我们添加一些基于标准的技巧,使切换开关能够自动匹配用户的系统暗黑模式设置。为此,我们将使用一个名为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);
};
这个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]);
[...]
解决另一个难题需要对 CSS 稍作调整。我们的代码无法更改 `<color>` 的值prefers-color-scheme,这使得在当前设置下很难强制启用暗黑模式。因此,我们将让颜色变量在 HTML 元素具有dark`<class>` 时发生变化(我们稍后会动态地将该类添加到元素中):
html {
--primaryBackgroundColor: white;
--primaryTextColor: black;
}
html.dark {
--primaryBackgroundColor: black;
--primaryTextColor: white;
}
最后,让我们更新函数体,根据条件是否为真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"
/>
);
};
🎉 大功告成!现在,无论直接点击开关还是更改系统的深色模式设置,页面主题都会自动更改。
可选的收尾工作
处理更复杂的样式
我发现 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);
}
}
mixin 本身的代码使用了&SCSS 特性来引用调用 mixin 的选择器,并@content允许将内容传递给它:
@mixin whenDark {
html.dark & {
@content;
}
}
(如果你深入查看沙盒代码,你会发现我还使用了 mixin 来设置颜色变量,因此所有 CSS 都使用相同的代码来确定深色模式是否处于活动状态)。
支持不使用 JavaScript 的用户
由于我们将 CSS 从使用prefers-color-scheme媒体查询改为依赖 JavaScript 代码中设置的类,我们无意中破坏了禁用 JavaScript 的用户的暗黑模式支持。(如果您没有预渲染网站,则不会出现此问题,因为对于禁用 JavaScript 的用户,网站可能根本不会显示。)
幸运的是,如果您使用的是上面的 mixin,那么恢复支持就相当简单——只需更新它,使其在媒体查询处于活动状态时也应用任何样式即可:
@mixin whenDark {
html.dark & {
@content;
}
@media (prefers-color-scheme: dark) {
& {
@content;
}
}
}
更多提示和资源
模拟深色模式
Chrome 的开发者工具允许您通过渲染选项卡模拟 prefers-color-scheme 值。
如果你使用的是 Mac,Safari 的开发者工具也允许你一键切换到深色模式:
记住用户的偏好
我还没试过这种方法,但它绝对值得探索。如果你的网站还没有数据持久化解决方案,那么 ` use-persisted-state`这个钩子非常适合用于暗黑模式切换。
从切换按钮外部查询深色模式
在我描述的这种设置中,唯一真正知道深色模式是否激活的组件是切换组件。对于简单的场景来说,这完全没问题,但如果其他 JavaScript 代码需要根据主题做出不同的行为呢?虽然我目前还没有遇到这种情况,但任何常用的状态共享方案都应该可以解决这个问题——无论是Context API、Redux 还是你网站已经在使用的其他任何技术。
浏览器支持
坏消息:我们的老朋友 Internet Explorer 不支持 CSS 变量。这意味着这种方法在 IE 上效果不佳——所有变量属性都会回退到它们的默认值/继承值(例如,文本颜色很可能是黑色)。如果您确实需要支持 IE,有几种选择——主要的选择是css-vars-ponyfill和基于 SASS 的回退方案。
资源
以下是一些您可能会觉得有用的其他资源(我确实觉得很有用):
-
假设你要写一篇关于深色模式的博客文章——即使你不打算写一篇关于深色模式的博客文章,这也是一个深入了解极端情况、辅助功能问题以及我还没有真正涵盖的其他要点的绝佳起点(抱歉,克里斯!)
-
Color.review——我最喜欢的选择实用色彩组合的网站。
-
CSS 自定义属性策略指南——一篇关于如何策略性地使用和思考 CSS 变量的优秀文章。
-
如何设计令人愉悦的深色主题——设计深色主题时需要记住的实用技巧。
-
支持网页内容中的深色模式——这里有一些处理深色模式下图像的实用技巧。
哇哦,真是一段精彩的旅程!感谢你一路陪伴我走到这里,如果你觉得这些内容对你有所帮助,我很想看看你的作品!
文章来源:https://dev.to/nw/adding-dark-mode-to-your-react-app-with-hooks-media-queries-and-css-variables-50h0


