使用 CSS 自定义属性在暗模式下调整可变字体粗细

Avatar of Greg Gibson
Greg Gibson

DigitalOcean 为您的旅程的每个阶段提供云产品。立即开始使用 $200 的免费积分!

黑色并不总是显瘦。

最近在为我的一个网站测试暗模式选项时,我亲身经历了 Robin Rendle 在 这篇文章 中提到的问题。当我切换到暗模式时,我的所有页面文本——标题和正文文字——似乎都变粗了。而且,无论我使用什么字体或尝试什么浏览器,结果都是一样的。

例如,以下是在 Windows 版 Chrome 中使用 Adobe 的 Source Sans Pro 时发生的情况

看到切换到暗模式后出现的模糊边缘了吗?

这并非错觉。浅色字符在深色背景上确实更重。我们可以放大以更好地观察

字符在暗模式下确实更粗!

当我们反转这些图像的暗模式部分时,这一点就变得非常明显了

We can really see the difference when putting the characters side-by-side on the same white background.
将字符并排放在同一个白色背景上,我们就可以真正看到区别。

一个解决方案

由于可变字体在浏览器中得到了广泛的支持,因此我们可以利用它们来解决此问题。以下三个面板展示了我们要努力实现的解决方案

顶部显示的是深色背景上的浅色文本。中间面板显示的是在暗模式下不改变任何字体粗细设置时发生的情况。底部的面板展示了我们稍微变薄的暗模式文本。第三个面板已调整为与浅色对应物的粗细一致,这也是我们在这里要实现的目标。

以下是如何实现这种改进的效果

  1. 通过以下方法之一在暗模式下减少 font-weight 属性
    1. 在暗模式媒体查询中直接手动更改每个 font-weight 赋值。
    2. 创建一个名为 --font-weight-multiplier 的自定义属性,并在暗模式下更改其值,然后用它乘以每个元素的默认 font-weight 值。
    3. 同样,但不是单独计算每个元素的 font-weight 属性,而是利用 CSS 变量作用域和通用选择器 (*) 在所有位置同时应用我们的乘法计算。
  2. 调整可变字体的等级 (“GRAD”) 轴。并非所有可变字体都支持此特定功能,但 Roboto Flex 支持。更改此轴值会改变字体的视觉粗细,而不会影响字母的宽度。
  3. 调整可变字体的暗模式 ("DRKM") 轴。Dalton Maag 的 aptly-named Darkmode 以及其同名的暗模式轴,非常适合这种情况。与 Roboto Flex 的等级轴一样,调整 Darkmode 的暗模式轴会改变字体的视觉粗细。但是,等级轴需要对值进行一些微调,而暗模式轴只需开启 (更细) 或关闭 (常规)。

第一组中的技术适用于大多数可变字体。Robin 在他的文章中使用的解决方案实际上是该组中的第一个项目。我将通过引入帮助我们在暗模式下自动调整字体粗细的自定义属性,来扩展该组中的第二和第三个项目。

第二和第三组涉及不太常见的 font-variation-settings 轴。尽管这些策略适用于较少的字体,但在可用时可能更可取。诀窍是在选择字体之前了解可变字体支持哪些功能。

我制作了一个演示页面,其中包括本文中介绍的所有策略。您可以看到一些不同的可变字体在亮模式下、在暗模式下不进行调整的情况下,以及在暗模式下应用了我们的解决方案以使字符变薄的情况下的外观。

除了上面列出的策略之外,还有一个选择: **什么都不做!** 如果您认为您的字体在亮模式和暗模式下看起来足够好,或者您目前没有足够的带宽来处理重排、元素大小调整、浏览器/显示不一致以及额外的 CSS 维护,那么您可能无需进行任何更改。专注于您网站的其他部分,并给自己留出时间以后重新审视此主题。

策略 1:减少 font-weight

大多数可变文本字体都有一个粗细轴,它允许我们在该字体可用的粗细范围内分配任何特定的 font-weight 值(例如 0-1000、300-800 等)。此策略中的每种技术都利用了对粗细轴的这种精确控制,以在暗模式下减少 font-weight 值。(对这种 font-weight 精度的需求也是使大多数非可变字体不适合此解决方案的原因。)

如果您使用的是本地可变字体,您可以在 Wakamai Fondue 中检查它们的轴和值范围

在 Wakamai Fondue 中,您可以查看任何本地字体的可变轴和范围。

请记住,如果您使用的是 @font-face 规则来加载字体,则应同时为每个字体设置 font-weight 范围

@font-face {
  src: url('Highgate.woff2') format('woff2-variations');
  font-family: 'Highgate';
  font-weight: 100 900;
}

如果您忽略此步骤,一些可变字体可能无法在当前的 Chromium 浏览器中正确反映特定的 font-weight 值。

在 Chrome 中将 Dalton Maag Highgate 的 font-weight 设置为 800,不指定 (左) 和指定 (右) @font-face 规则中的 font-weight 范围。

基本解决方案:手动输入每个粗细

这是我们大多数人可能采用的技术。我们创建一个暗模式媒体查询,在其中输入一些比默认值略低的 font-weight 值。

/* Default (light mode) CSS */ 
body {
  font-weight: 400;
}

strong, b, th, h1, h2, h3, h4, h5, h6 {
  font-weight: 700;
}

/* Dark mode CSS */
@media (prefers-color-scheme: dark) {
  body {
    font-weight: 350;
  }

  strong, b, th, h1, h2, h3, h4, h5, h6 {
    font-weight: 600;
  }
}

它可以工作,而且维护起来也很简单——只要我们不打算在网站上添加或编辑任何其他粗细!但是,如果我们确实开始合并更多粗细,它会很快变得难以管理。请记住,在 prefers-color-scheme 媒体查询的外部和内部都要输入每个选择器/属性组合。我们必须进行一些手动计算(或猜测)才能确定每个元素的暗模式属性值。

创建一个粗细乘数自定义属性,并在设置元素的粗细时将其用于计算

我通常会遵循 Mike Riethmuller 的信条,即“媒体查询仅用于更改自定义属性的值”。这就是我们在此解决方案中所做的改进。我们无需在暗模式的内外输入所有元素的字体粗细,我们只需要在媒体查询中输入 --font-weight-multiplier 自定义属性

@media (prefers-color-scheme: dark) {
  :root {
    --font-weight-multiplier: .85;
  }
}

然后,对于样式表中的所有 font-weight 属性,我们将变量的值乘以我们为每个元素首选的默认粗细值——因此在暗模式下字体粗细降低了 15%。如果我们不在暗模式下,我们将默认粗细值乘以 1,这意味着它根本不会改变。

我的意思是,通常情况下,我们会使用它来设置正文字体粗细为 400

body {
  font-weight: 400;
}

对于此解决方案,我们使用此代码

body {
  font-weight: calc(400 * var(--font-weight-multiplier, 1));
}

var() 函数中,请注意我们的变量具有 一个备用值 1。由于 --font-weight-multiplier 仅在暗模式下设置,因此其余时间将使用此备用值。因此,默认情况下,我们正文文本的字体粗细保持为 400 (400*1)。但在暗模式下,粗细会降低到 340 (400*.85)。

我们还会对粗体元素执行此操作

strong, b, th, h1, h2, h3, h4, h5, h6 {
  font-weight: calc(700 * var(--font-weight-multiplier, 1));
}

这些粗细将在暗模式下从 700 降低到 595 (700*.85)。

我们可以对任何其他元素使用相同的技术,我们希望在默认情况下将 font-weight 设置为除 400 之外的值。

我使用的是 --font-weight-multiplier 的值 .85,因为我发现它对于大多数字体(例如 Adobe Source Sans Pro,我在本文的多数演示中使用的免费字体)都是一个很好的通用值。但您可以随意调整该数字。

以下是它的组合外观

/* DARK-MODE-SPECIFIC CUSTOM PROPERTIES */
@media (prefers-color-scheme: dark) {
  :root {
    --font-weight-multiplier: .85;
  }
}

/* DEFAULT CSS STYLES... */
body {
  font-weight: calc(400 * var(--font-weight-multiplier, 1));
}

strong, b, th, h1, h2, h3, h4, h5, h6 {
  font-weight: calc(700 * var(--font-weight-multiplier, 1));
}

创建一个粗细乘数变量,并自动计算和将其应用于所有元素

在使用多个 CSS 自定义属性时,我认为我们中的许多人都会坚持“按需设置并在所有位置手动应用”的方法。这就是前一个解决方案所做的事情。我们在 :root 中设置自定义属性值(或使用备用值),在媒体查询中再次设置它,然后在每次分配 font-weight 值时,使用 calc()var() 函数在整个样式表中应用它。

代码可能看起来像这样

h1 {
  font-weight: calc(800 * var(--font-weight-multiplier, 1);
}

summary {
  font-weight: calc(600 * var(--font-weight-multiplier, 1);
}

但是,当我们对各种元素使用此技术时,您可以看到每次分配 font-weight 值时,我们都需要执行这三件事

  • 包含 calc() 函数
  • 包含 var() 函数
  • 记住 --font-weight-multiplier 自定义属性的名称和默认值

相反,我最近开始为某些任务反转这种方法,利用 CSS 变量作用域使用“设置所有地方,应用一次”的方法。对于这种技术,我用一个 `--font-weight` 变量替换样式表中的所有 `font-weight` 属性,为了简便起见,保留名称不变,只是将连字符改为破折号。然后,我将此值设置为特定选择器的默认权重(例如,正文文本为 400)。目前不需要使用 `calc()` 或 `var()`。这就是我们如何 **设置所有地方** 的。

然后,我们 **应用一次**,在样式表中使用一个单独的 `font-weight` 属性,通过通用选择器设置每个文本元素的权重。修改上面的代码片段,我们现在将有以下内容

h1 {
  --font-weight: 800;
}

summary {
  --font-weight: 600;
}

* {
  font-weight: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1);
}

`calc()` 函数将我们的每个 `--font-weight` 自定义属性乘以我们的乘数变量,然后 `font-weight` 属性将值应用于其相应的元素。

没有必要为样式表中的每个自定义属性只使用一个 `var()`。但我经常喜欢在执行计算和/或使用辅助变量时这样做,就像我们在这里做的那样。也就是说,虽然这无疑是调整字体权重的 **最聪明** 的技术,但这并不意味着它是所有项目 **最佳** 的技术。至少存在一个严重的问题。

使用通用选择器技术的首要优势 - 它应用于所有内容 - 也带来了其主要风险。可能有一些元素我们不想变细!例如,如果我们的表单元素在暗模式下保留浅色背景上的深色文本,它们仍然可能会被通用选择器覆盖。

有一些方法可以减轻这种风险。我们可以将 `*` 替换为一个包含要变细的元素列表的长选择器字符串(让他们选择加入计算)。或者,我们可以为我们不想受影响的元素硬编码字体权重(选择退出)

* {
  font-weight: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1));
}

button, input, select, textarea {
  font-weight: 400;
}

最终,这样的修复可能会使代码与之前的方法一样复杂。因此,您必须评估哪种方法适合您的项目。如果您仍然担心性能、代码复杂性,或者认为这种技术可能会引入不希望的(甚至是不可预测的)结果,那么之前的方法可能是最安全的。

最终代码

/* DEFAULT CUSTOM PROPERTIES */
:root {
  --font-weight: 400;
  --font-weight-multiplier: 1;
}
strong, b, th, h1, h2, h3, h4, h5, h6 {
  --font-weight: 700;
}

/* DARK-MODE-SPECIFIC CUSTOM PROPERTIES */
@media (prefers-color-scheme: dark) {
  :root {
    --font-weight-multiplier: .85;
  }
}

/* APPLYING THE CUSTOM PROPERTIES... */
* {
  font-weight: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1));
}

我们不需要在上面的代码中设置默认的 `--font-weight: 400` 和 `--font-weight-multiplier: 1` 自定义属性,因为我们在 `var()` 函数中包含了回退值。但是,随着代码变得越来越复杂,我经常喜欢在逻辑位置分配它们,以防我以后想要查找和更改它们。

关于这种策略的最后一点:我们也可以使用 `font-variation-settings` 属性和 `“wght”` 轴值来应用权重,而不是 `font-weight`。如果您使用的是具有多个轴的字体,您可能发现通过这种方式进行所有字体调整更易于管理。我知道至少有一款字体(Type Network 的 Roboto Flex,我们将在本文后面使用它)有 13 个轴!

以下是如何通过 `font-variation-settings` 属性应用我们的解决方案

* {
  --wght: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1));
  font-variation-settings: "wght" var(--wght);
}

策略 1 附录:处理 `letter-spacing`

降低字体权重的副作用之一是,对于大多数非等宽字体,它还会缩小字符宽度。

以下是使用我们的乘数使 Source Sans Pro 变轻时发生的情况。下面前两个面板显示了默认情况下在浅色模式和深色模式下的 Source Sans Pro。最下面的面板显示了较浅的版本。

Adobe 的 Source Sans Pro 在浅色模式、默认深色模式和变细的深色模式下。

在没有调整的情况下,浅色模式和深色模式下的字符宽度相同。但是,当我们降低字体权重时,这些字符现在占用的空间更小。您可能不喜欢此更改对您的流或元素大小的影响(例如,更窄的按钮)。有些设计师认为在深色模式下 添加字母间距 是一个好主意。因此,如果您愿意,可以创建另一个自定义属性来添加一些空间。

实现一个用于字母间距的自定义属性

就像我们对 `font-weight` 乘数变量所做的那样,我们将创建一个字母间距变量,它具有一个默认值,该值将在深色模式下被覆盖。在我们的默认(浅色模式)`:root` 中,我们暂时将新的 `--letter-spacing` 自定义属性设置为 0

:root {
  /* ...other custom variables... */
  --letter-spacing: 0;
}

然后,在我们的深色模式查询中,我们将值提高到大于 0 的值。我在这里将其输入为 `0.02ch`(与 `--font-weight-multiplier` 值为 0.85 相结合效果很好)。如果您愿意,您甚至可以巧妙地根据字体权重和/或大小进行一些计算来微调它。但我目前将使用这个硬编码的值

@media (prefers-color-scheme: dark) {
  :root {
    /* ...other custom variables... */
    --letter-spacing: .02ch;
  }
}

最后,我们通过通用选择器(回退值为 0)应用它

* {
  /* ...other property settings... */
  letter-spacing: var(--letter-spacing, 0);
}

注意:虽然我在此示例中使用了 `ch` 单位,但如果您愿意,使用 `em` 也行。对于 Source Sans Pro,`0.009em` 的值大约等于 `0.02ch`。

以下是具有字母间距的字体权重乘数的完整代码

/* DEFAULT CSS CUSTOM PROPERTIES */
:root {
  --font-weight: 400;
  --font-weight-multiplier: 1;
  --letter-spacing: 0;
}

strong, b, th, h1, h2, h3, h4, h5, h6 {
  --font-weight: 700;
}

/* DARK MODE CSS CUSTOM PROPERTIES */
@media (prefers-color-scheme: dark) {
  :root {
    /* Variables to set the dark mode bg and text colors for our demo. */
    --background: #222;
    --color: #fff;

    /* Variables that affect font appearance in dark mode. */
    --font-weight-multiplier: .85;
    --letter-spacing: .02ch;
  }
}

/* APPLYING CSS STYLES... */
* {
  font-weight: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1));
  letter-spacing: var(--letter-spacing, 0);
}

body {
  background: var(--background, #fff);
  color: var(--color, #222);
}

具有等宽字符的字体(也称为多重字体)

除了等宽字体之外,还有一些其他专门设计的字体,它们的单个字符无论权重如何,都占用相同数量的水平空间。例如,如果一个“i”在 400 权重下占用五个水平像素的空间,而一个“w”在相同权重下占用 13 个像素,那么当它们的权重增加到 700 时,它们仍然分别占用五个和 13 个像素。

Arrow Type 的 Recursive Sans 就是这样一种字体。下图显示了 Recursive 的字符如何在浅色模式、默认深色模式和深色模式下使用我们的字体权重乘数分别保持相同的宽度

Arrow Type 的 Recursive 的字符无论字体权重如何都保持相同的宽度。

像 Recursive 这样的多重字体,旨在让您在深色模式下更改字体权重时无需调整字母间距。您的元素大小和页面流将保持不变。

策略 2:调整可变字体的等级轴

等级轴(`“GRAD”`)会更改字体的表观权重,而不会更改其实际的 `font-weight` 值或其字符的宽度。在使用具有此轴的字体时,您可能根本不需要我们的字体权重乘数变量。

对于 Type Network 的免费 Roboto Flex 字体,等级 -1 是最细的,0(默认)是正常的,1 是最粗的。对于这种字体,我首先将它的等级轴的值设置为大约 -0.75 用于深色模式。

Roboto Flex 在浅色模式、默认深色模式和“GRAD” 设置为 -0.75 的深色模式下
:root {
  --GRAD: 0;
}

@media (prefers-color-scheme: dark) {
  :root {
    --GRAD: -.75;
  }
}

body {
  font-variation-settings: "GRAD" var(--GRAD, 0);
}

因此,如果您的字体可用,调整等级轴似乎是完美的解决方案,对吧?好吧,也许吧。在考虑它时,需要注意一些事项。

首先,并非所有字体的等级范围都是从 -1 到 1。一些字体的范围从 0 到 1。 至少有一种字体 使用百分比,使 100 成为默认值。其他字体将等级比例与字体权重对齐,因此范围可能是 100-900 之类的东西。如果您想在后一种情况下使用等级轴,您可能必须将所有字体的权重在所有地方都设置为 400 的默认值,然后使用等级轴进行所有权重更改。对于深色模式,您将需要像我们的字体权重乘数解决方案中所做的那样处理等级 - 将乘数应用于 `font-variation settings` 中的 `“GRAD”` 轴。

第二个问题是,一些字体不允许您将字体的等级设置为低于其默认权重的值。因此,等级根本无法使字体变细。Apple 的 San Francisco 字体(可以通过在 Apple 设备上使用 `font-family: system-ui;` 进行测试)同时存在这两个问题。 截至 macOS Catalina,San Francisco 具有等级轴。它的比例与字体权重一致,其最小值为 400。

San Francisco 的等级轴和权重轴使用相同的比例,但具有不同的范围。

由于我们无法将等级设置为低于 400 的值,因此我们无法在深色模式下将字体的默认权重从 400 变细。如果我们想降低,我们将需要降低权重轴的值,而不是等级。

策略 3:调整可变字体的深色模式轴

截至目前,只有一个字体包含暗黑模式("DRKM")轴:Dalton Maag 的 Darkmode

暗黑模式轴本质上是一个没有微调的粗细轴。只需将其开启(1)即可在暗黑模式下获得更细的显示效果,而将其关闭(0,默认值)则保持正常显示。

暗黑模式在亮模式下的显示效果,以及在暗黑模式下,“DRKM” 未设置和设置为 1 时的显示效果。
:root {
  --DRKM: 0;
}

@media (prefers-color-scheme: dark) {
  :root {
    --DRKM: 1;
  }
}

body {
  font-variation-settings: "DRKM" var(--DRKM, 0);
}

我非常喜欢 Darkmode 字体。但需要注意的是,它需要商业许可才能用于专业用途。Dalton Maag 提供试用版,仅可用于“学术、推测或演示目的”。我希望这种字体能够成为 Dalton Maag 其他字体家族添加暗黑模式轴的先锋,并希望其他字体铸造厂也能效仿!

其他需要考虑的因素

我们已经介绍了一些在暗黑模式下使用可变字体的策略。但与大多数事情一样,还有一些其他因素需要考虑,这些因素可能会让你倾向于某一种解决方案。

高分辨率(“视网膜”)屏幕上的暗黑模式

在像素密度更高的屏幕(例如,大多数现代手机、MacBook、iMac 等)上,暗黑模式的加粗效果通常不太明显。因此,你可能不希望在这些屏幕上将字体变得更细,甚至完全不改变字体粗细!

如果你仍然想要稍微淡化字体,可以添加另一个媒体查询,使效果不那么明显。根据你使用的解决方案,你可以将 --font-weight-multiplier 值提高到更接近 1 的位置,将 --GRAD 值提高到更接近 0 的位置,或者完全禁用 --DRKM(因为它要么开启要么关闭,没有中间状态)。

如果你添加了此查询,请记住将其放置在原始 prefers-color-scheme 查询下方,否则它可能无效。媒体查询不会增加 CSS 的特殊性,因此它们的顺序很重要!

@media (prefers-color-scheme: dark) and (-webkit-min-device-pixel-ratio: 2), 
       (prefers-color-scheme: dark) and (min-resolution: 192dpi) { 
  :root {
    --font-weight-multiplier: .92;
    /* Or, if you're using grade or darkmode axis instead: */
    /* --GRAD: -.3; */
    /* --DRKM: 0; */
  }
}

如果你不希望在暗黑模式下高密度屏幕上的字体完全变淡,可以将你原始的暗黑模式 prefers-color-scheme 查询更新为以下内容,以排除这些屏幕

@media (prefers-color-scheme: dark) and (-webkit-max-device-pixel-ratio: 1.9), 
       (prefers-color-scheme: dark) and (max-resolution: 191dpi) { 

  /* Custom properties for dark mode go here. */

}

混合具有不同轴的字体(以及混合可变字体和非可变字体)

如果你在网站上使用不止一种字体,你需要考虑这些调整可能对所有字体产生的影响。例如,如果你使用多种具有交叉轴的字体,你可能会意外地将多种策略的效果组合起来(例如,同时降低粗细和字重)。

如果你的样式表包含针对多种字体/轴的解决方案,那么对具有多个轴的字体(例如,本例中的 Roboto Flex,它同时具有粗细和字重轴)的影响可能是累积的。

如果你的网站上的所有字体都是可变字体,并且它们的粗细轴具有匹配的刻度和范围(例如,如果它们都在 -1 到 1 之间),那么我建议你使用这种解决方案。但是,如果你计划以后添加不符合这些条件的其他字体,则需要重新审视这个问题。同样,如果暗黑模式轴变得更加普遍,你也需要重新审视这个问题。

如果你的所有字体都是可变字体,但它们并不都共享相同的轴(例如,粗细和暗黑模式),那么只使用 --font-weight-multiplier 自定义属性可能是最安全的做法。

最后,如果你混合使用可变字体和非可变字体,请注意,非可变字体不会通过任何这些解决方案改变外观,但也有一些例外。例如,如果你使用 font-weight 属性与字体重量乘数一起使用,那么你网站上的一些(但可能不是全部)字体重量可能会发生足够大的变化,以移动到下一个较低的重量名称。

假设你的网站包含一种字体,它有三种字重:常规 (400)、半粗体 (600) 和粗体 (700)。在暗黑模式下,你的粗体文本可能会淡化到足以显示为半粗体。但你的常规字体仍然会保持常规(因为这是网站上包含的最低字重)。如果你想避免这种不一致,可以应用 font-variation-settings(而不是 font-weight)来设置你的可变字体字重,这样你的非可变字体就不会受到影响。它们只会始终在暗黑模式下保持默认字重。

最后

当互补的技术在同一时间获得广泛使用时,这始终是一种令人愉快的巧合。随着暗黑模式和可变字体的流行,我们可以利用后者来缓解前者带来的挑战之一。通过将 CSS 自定义属性与字重、粗细和暗黑模式轴结合使用,我们可以在亮模式和暗黑模式下为文本的外观带来一致性。

你可以访问 我的 交互式演示,其中包含本文中提到的字体和轴。