React中的黑暗模式。一个深入的指南

1,988 阅读7分钟

随着我们在网络上向更好、更方便的用户体验迈进,黑暗模式已经成为网络应用的一个主流功能。当涉及到黑暗模式的开发时,它不仅仅是添加一个简单的切换按钮和管理CSS变量。这里我们将讨论在React应用程序中创建一个完整的黑暗模式体验。

以下是我们将讨论的内容。

  • 使用系统设置
  • 使用CSS变量管理主题
  • 使用react-toggle实现颜色方案的切换
  • 使用use-persisted-state存储用户喜欢的模式
  • 选择适合更多受众的颜色组合
  • 处理黑暗模式下的图像

你可以在Github上找到演示程序它的代码

使用系统设置

当用户登陆他们的网站时,没有人想伤害他们的眼睛!最好的做法是根据设备的设置来设置应用程序的主题。CSS媒体查询,一般以响应式设计的用法而闻名,也帮助我们检查其他设备的特性。

在这里,我们将使用 [prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)来提供darklight ,或no-preference ,基于设备选择的颜色方案。

即使是最简单的形式,这也可以帮助我们为网络应用添加一个黑暗模式。

@media (prefers-color-scheme: dark) {
  background-color: #1F2023
  color: #DADADA
}

像其他媒体查询一样,当设备的颜色方案被设置为深色时,该块的样式将被应用。把它放在一些组件的样式中,会看起来像这样。

import { styled } from '@linaria/react';

const Text = styled.p`
  margin: 12px;
  color: #1F2023;
  background-color: #FAFAFA;
  @media (prefers-color-scheme: dark) {
    background-color: #1F2023
    color: #DADADA
  }
`;

这在开始时是好的,但不能一直在每个组件中添加这些样式。在这种情况下,CSS变量就是答案了。

使用CSS变量管理主题

CSS变量是网页样式设计中缺失很久很久的一个工具。现在它们可以在所有的浏览器上使用,CSS变得更加有趣而不那么痛苦了。

CSS变量的作用范围是它们所声明的元素,并参与级联(即元素是子元素的覆盖值)。

我们可以利用CSS变量来定义我们应用程序的主题。这里有一个小片段来回顾一下CSS变量是如何声明的。

body {
  --color-background: #FAFAFA;
  --color-foreground: #1F2023;
}

为了在我们的组件中使用这些变量,我们将把颜色代码与变量交换。

const Text = styled.p`
  margin: 12px;
  color: var(--color-foreground);
  background-color: var(--color-background);
`;

现在,我们的颜色是通过CSS变量定义的,我们可以在我们的HTML树的顶部改变数值(例如,<body> ),并且可以在所有的元素上看到反映。

body {
  --color-background: #FAFAFA;
  --color-foreground: #1F2023;

  @media (prefers-color-scheme: dark) {
    --color-background: #1F2023;
    --color-foreground: #EFEFEF;
  }
}

实现颜色方案的切换

在这一点上,我们有一个最简单的解决方案,它基于设备的偏好而工作。现在,我们必须为那些不支持黑暗模式的设备进行调整。

在这种情况下,我们必须让用户很容易为我们的网络应用设置他们的偏好。我选择了react-toggle,以使我们的解决方案在a11y时更有优势,同时具有良好的美感。这可以通过简单的buttonuseState 来实现。

Gif of a toggle slider turning dark mode off and on

下面是我们的切换组件的样子。

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

export const DarkModeToggle: React.FC = () => {
  const [isDark, setIsDark] = useState<boolean>(true);

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

这个组件将持有用户选择的模式,但默认值呢?我们的CSS解决方案尊重设备的偏好。为了在我们的react组件中提取媒体查询结果,我们将利用react-responsive。在引擎盖下,它使用 [Window.matchMedia()](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia)并在查询的输出改变时重新显示我们的组件。

该按钮的更新版本看起来像下面这样。

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

export const DarkModeToggle: React.FC = () => {
  const [isDark, setIsDark] = useState<boolean>(true);

  const systemPrefersDark = useMediaQuery(
    {
      query: '(prefers-color-scheme: dark)',
    },
    undefined,
    (isSystemDark: boolean) => setIsDark(isSystemDark)
  );

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

useMediaQuery 钩子接受一个查询、初始值和一个onChange 处理程序,该处理程序在查询的输出发生变化时被触发。

模拟浏览器的黑暗模式

现在我们的组件将与设备的偏好同步,它的值也将相应地更新。但我们如何测试它是否做对了呢?

感谢对开发者友好的浏览器,我们可以从浏览器检查器中模拟设备首选项;这里是它在Firefox中的样子。

Gif of dark mode toggling on and off in Firefox

现在是时候把我们的切换组件的状态变化与CSS联系起来了。这可以通过几种不同的技术来完成。在这里,我们选择了最简单的方法:在根HTML标签上添加一个类,让CSS变量来完成剩下的工作。

为了适应这一点,我们将更新我们的body标签的CSS。

body {
  --color-background: #FAFAFA;
  --color-foreground: #1F2023;

  &.dark {
    --color-background: #1F2023;
    --color-foreground: #EFEFEF;
  }
}

这里是我们根据状态来添加和删除类的效果。

...
useEffect(() => {
  if (isDark) {
    document.body.classList.add('dark');
  } else {
    document.body.classList.remove('dark');
  }
}, [isDark]); 
...

使用use-persisted-state来存储用户的首选模式

如果我们把用户的首选颜色方案保留在组件的状态中,可能会出现问题,因为我们将无法在这个组件之外获得这些值。而且,一旦我们的应用程序再次被安装,它就会消失。这两个问题都可以用不同的方法解决,包括用React Context或其他的状态管理方法。

另一个解决方案是使用use-persisted-state。这将帮助我们满足所有的要求。它将状态与localStorage ,并在应用程序在浏览器的不同标签中打开时保持状态同步。

我们现在可以将我们的黑暗模式状态移到一个自定义钩子中,该钩子封装了所有与媒体查询和持久化状态有关的逻辑。下面是这个钩子的样子。

import { useEffect, useMemo } from 'react';
import { useMediaQuery } from 'react-responsive';
import createPersistedState from 'use-persisted-state';

const useColorSchemeState = createPersistedState('colorScheme');

export function useColorScheme(): {
  isDark: boolean;
  setIsDark: (value: boolean) => void;
} {
  const systemPrefersDark = useMediaQuery(
    {
      query: '(prefers-color-scheme: dark)',
    },
    undefined,
  );
  const [isDark, setIsDark] = useColorSchemeState<boolean>();
  const value = useMemo(() => isDark === undefined ? !!systemPrefersDark : isDark,
    [isDark, systemPrefersDark])
  useEffect(() => {
    if (value) {
      document.body.classList.add('dark');
    } else {
      document.body.classList.remove('dark');
    }
  }, [value]);
  return {
    isDark: value,
    setIsDark,
  };
}

现在,切换按钮组件将变得更加简单。

/**
 *
 * ColorSchemeToggle
 *
 */
import Toggle from 'react-toggle';
import { useColorScheme } from 'platform/ColorScheme';
import { DarkToggle } from './Styled';

const ColorSchemeToggle: React.FC = () => {
  const { value, setValue } = useColorScheme();
  return (
    <DarkToggle>
      <Toggle
        checked={value === 'dark'}
        onChange={(event) => setValue(event.target.checked ? 'dark' : 'light')}
        icons={{ checked: '🌙', unchecked: '🔆' }}
        aria-label="Dark mode"
      />
    </DarkToggle>
  );
};

export default ColorSchemeToggle;

选择黑暗主题的颜色

虽然黑暗模式本身可以被认为是一个无障碍功能,但我们应该专注于让更多的人可以使用这个功能。

我们在演示中利用了react-toggle来确保用于改变颜色方案的按钮遵循所有a11y标准。另一个重要的部分是在深色和浅色模式下对背景和前景颜色的选择。在我看来,colors.review是一个测试颜色之间对比度的很好的工具;拥有AAA级的颜色使我们的应用程序更容易导航,看起来更舒服。

Gif of a React app with dark mode toggling under a photo of flowers

在黑暗模式下处理图像

为了获得更好的美感,我们的页面通常都有明亮的图片。在黑暗模式下,明亮的图像可能会成为用户的不适。

有几种技术可以避免这些问题,包括为两种模式使用不同的图像和改变SVG图像的颜色。一种方法是在所有图像元素上使用CSS过滤器;当明亮的图像出现在用户的画布上时,这将有助于降低眼睛的疲劳。

为了实现这一点,我们的全局样式将如下所示。

body {
  --color-background: #FAFAFA;
  --color-foreground: #1F2023;

  --image-grayscale: 0;
  --image-opacity: 100%;

  &.dark {
    --color-background: #1F2023;
    --color-foreground: #EFEFEF;

    --image-grayscale: 50%;
    --image-opacity: 90%;
  }
}

img,
video {
  filter: grayscale(var(--image-grayscale)) opacity(var(--image-opacity));
}

结论

今天,网络应用中的可访问性不仅仅是一种实用性。相反,它是基本要求之一。在这方面,当实施黑暗模式时,应该被视为一个完整的功能,需要非常关注,就像任何其他关键功能一样。

在这篇文章中,我们建立了一个全面实现黑暗模式的方法;如果你觉得我错过了什么,请在评论中告诉我。

The postDark mode in React:深度指南首次出现在LogRocket博客上。