使用变量进行主题化:全局变量和 局部变量

Avatar of Andrés Galante
Andrés Galante 发表于

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用,获得 $200 免费赠金!

Cliff Pyles 为本文做出了贡献。

设置 CSS 变量以对设计系统进行主题化可能会很棘手:如果它们的范围太小,系统将失去一致性。如果它们的范围太大,您将失去粒度。

也许我们可以解决这两个问题。我想尝试将设计系统变量归纳为两种类型:**全局**变量和**组件**变量。全局变量将使我们的组件之间保持一致性。组件变量将为我们提供粒度和隔离性。让我以一个相当简单的组件为例来向您展示如何做到这一点。

提醒一下,我将在本文中使用 CSS 变量,但该概念也适用于预处理器变量。

全局范围的变量

系统范围的变量是为保持组件之间的一致性而定义的通用概念。

.alert 组件为例,假设我们希望在所有边距和填充上保持一致性。我们可以先定义全局间距

:root {
  --spacer-sm: .5rem;
  --spacer-md: 1rem;
  --spacer-lg: 2rem;
}

然后在我们的组件上使用

/* Defines the btn component */
.btn {
  padding: var(--spacer-sm) var(--spacer-md);
}

/* Defines the alert component */
.alert {
  padding: var(--spacer-sm) var(--spacer-md);
}

这种方法的主要优点是

  • 它为间距生成单一事实来源,并为使用我们系统对其进行自定义的作者提供单一位置。
  • 它实现了 一致性,因为每个组件都遵循相同的间距。
  • 它为设计师和开发人员提供了一个共同的参考点。只要设计师遵循相同的间距限制,代码转换就是无缝的。

但它也带来了一些问题

  • 系统通过生成依赖树而失去模块化。由于组件依赖于全局变量,因此它们不再是隔离的。
  • 它不允许作者在不覆盖 CSS 的情况下自定义单个组件。例如,要更改警报的填充而不生成系统范围的变化,他们必须覆盖警报组件
.alert {
  padding-left: 1rem;
  padding-right: 1rem;
}

Chris Coyier 在这篇文章中解释了使用自定义元素和全局变量进行主题化的想法。

组件范围的变量

正如 Robin Rendle 在他的文章中所解释的那样,组件变量的作用域限定为每个模块。如果我们使用这些变量生成警报,我们将得到

.alert {
  --alert-color: #222;
  
  color: var(--alert-color);
  border-color: var(--alert-color);
}

主要优点是

  • 它创建了一个具有隔离组件的模块化系统。
  • 作者可以对组件进行精细控制,而无需覆盖它们。他们只需重新定义变量的值。

没有办法保持组件之间的一致性或使用此方法进行系统范围的更改。

让我们看看如何两全其美!

两层主题化系统

解决方案是一个两层主题化系统,其中全局变量始终通知组件变量。这些层中的每一层都遵循一组非常具体的规则。

第一层:全局变量

使用全局变量的主要原因是保持一致性,它们遵循以下规则

  • 它们以单词 global 为前缀,并遵循公式 --global--concept--modifier--state--PropertyCamelCase
    • concept 是指像 spacermain-title 这样的东西
    • state 是指像 hoverexpanded 这样的东西
    • modifier 是指像 smlg 这样的东西
    • PropertyCamelCase 是指像 BackgroundColorFontSize 这样的东西
  • 它们是概念,永远不会绑定到元素或组件
    • 这是错误的:--global-h1-font-size
    • 这是正确的:--global--main-title--FontSize

例如,全局变量设置如下所示

:root {
  /* --global--concept--size */
  --global--spacer--sm: .5rem;
  --global--spacer--md: 1rem;
  --global--spacer--lg: 2rem;

  /* --global--concept--PropertyCamelCase */
  --global--main-title--FontSize: 2rem;
  --global--secondary-title--FontSize: 1.8rem;
  --global--body--FontSize: 1rem;

  /* --global--state--PropertyCamelCase */
  --global--hover--BackgroundColor: #ccc;
}

第二层:组件变量

第二层的作用域限定为可主题化的组件属性,并遵循以下规则

  • 假设我们正在编写 BEM,它们遵循以下公式:--block__element--modifier--state--PropertyCamelCase
    • block__element--modifier 选择器名称类似于 alert__actionsalert--primary
    • state 是指像 hoveractive 这样的东西
    • 如果您没有编写 BEM 类名,则适用相同的原则,只需将 block__element--modifier 替换为您的 classname
  • 组件范围变量的值始终由全局变量定义
  • 组件变量始终具有默认值作为回退,以防组件不依赖于全局变量

例如

.alert {
  /* Component scoped variables are always defined by global variables */
  --alert--Padding: var(--global--spacer--md);
  --alert--primary--BackgroundColor: var(--global--primary-color);
  --alert__title--FontSize: var(--global--secondary-title--FontSize);

  /* --block--PropertyCamelCase */
  padding: var(--alert--Padding, 1rem); /* Sets the fallback to 1rem. */
}

/* --block--state--PropertyCamelCase */
.alert--primary {
  background-color: var(--alert--primary--BackgroundColor, #ccc);
}

/* --block__element--PropertyCamelCase */
.alert__title {
  font-size: var(--alert__title--FontSize, 1.8rem);
}

您会注意到我们正在使用全局变量定义局部范围的变量。这对系统的工作至关重要,因为它允许作者将整个系统主题化。例如,如果他们想更改所有组件的主要颜色,他们只需重新定义 --global--primary-color

另一方面,每个组件变量都有一个默认值,因此组件可以独立存在,它不依赖于任何东西,作者可以单独使用它。

此设置允许组件之间保持一致性,它在设计师和开发人员之间生成通用语言,因为我们可以像设计师的缓冲器一样在 Sketch 中设置相同的全局变量,并且它为作者提供了精细的控制。

为什么这个系统有效?

在一个理想的世界中,我们作为设计系统的创建者,希望“作者”或我们系统的用户在不修改的情况下实施它,但当然,世界并不理想,这永远不会发生。

如果我们允许作者在不必覆盖 CSS 的情况下轻松地为系统设置主题,我们不仅会让他们的生活更轻松,还会降低破坏模块的风险。归根结底,可维护的系统就是一个好的系统。

两层主题化系统生成模块化和隔离的组件,作者可以在其中在全局和组件级别对其进行自定义。例如

:root {
  /* Changes the secondary title size across the system */
  --global--secondary-title--FontSize: 2rem;
}

.alert {
  /* Changes the padding on the alert only */
  --alert--Padding: 3rem;
}

哪些值应该成为变量?

CSS 变量为代码打开了窗口。我们允许作者进入的越多,系统就越容易出现实现问题。

为了保持一致性,请为除布局值之外的所有内容设置全局变量;您不希望作者破坏布局。作为一般规则,我建议允许访问您愿意提供支持的所有组件。

对于我正在开发的开源设计系统 PatternFly 的下一个版本,我们将允许自定义几乎所有与布局无关的内容:颜色、间距、排版处理、阴影等。

整合一切

为了演示这个概念,我创建了一个 CodePen 项目

全局变量嵌套在 _global-variables.scss 中。它们是保持系统一致性的基础,并允许作者进行全局更改。

有两个组件:alertbutton。它们是隔离的模块化实体,具有范围变量,允许作者微调组件。

请记住,开发者会将我们的系统作为他们项目中的一个依赖项。通过让他们使用 CSS 变量来修改系统的外观,我们正在创建一个可靠的代码库,这对于系统的创建者来说更容易维护,对于使用该系统的开发者来说也更容易实现、修改和升级。

例如,如果开发者想要

  • 将整个系统的 primary 颜色更改为粉色;
  • 将按钮上的 danger 颜色更改为橙色;
  • 并将警告框的左侧填充更改为 2.3rem

…那么可以这样做:

:root {
  /* Changes the primary color on both the alert and the button */
  --global--primary--Color: hotpink;
}

.button {
  /* Changes the danger color on the button only without affecting the alert */
  --button--danger--BackgroundColor: orange;
  --button--danger--hover--BorderColor: darkgoldenrod;
}

.alert {
  /* Changes the padding left on the alert only without affecting the button */
  --alert--PaddingLeft: 2.3rem;
}

设计系统代码库保持不变,它只是一个更好的依赖项。

我知道这只是其中一种方法,而且我相信还有其他方法可以成功地在系统上设置变量。请在评论中告诉我您的想法,或者给我发推文。我很想听听您在做什么,并从中学习。