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
是指像spacer
或main-title
这样的东西state
是指像hover
或expanded
这样的东西modifier
是指像sm
或lg
这样的东西PropertyCamelCase
是指像BackgroundColor
或FontSize
这样的东西
- 它们是概念,永远不会绑定到元素或组件
- 这是错误的:
--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__actions
或alert--primary
state
是指像hover
或active
这样的东西- 如果您没有编写 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
中。它们是保持系统一致性的基础,并允许作者进行全局更改。
有两个组件:alert
和 button
。它们是隔离的模块化实体,具有范围变量,允许作者微调组件。
请记住,开发者会将我们的系统作为他们项目中的一个依赖项。通过让他们使用 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;
}
设计系统代码库保持不变,它只是一个更好的依赖项。
我知道这只是其中一种方法,而且我相信还有其他方法可以成功地在系统上设置变量。请在评论中告诉我您的想法,或者给我发推文。我很想听听您在做什么,并从中学习。
我们在设计系统的组件库中也使用了这种模式,但使用的是 CSS-in-JS(在我们的例子中是glamorous),而不是 CSS/Sass/BEM。这对我们来说很有效,而且我们的用户也很喜欢这种定制的灵活性。
如果您感兴趣:我们的全局主题、组件的局部主题以及该组件的源代码。
不过,我不同意局部主题变量必须引用全局主题变量的说法。虽然是例外情况,但在我们的系统中,局部变量有时可以只是一个直接值,我认为这是一个共同的需求。
caniuse.com 表示不支持 IE11。不幸的是,我的公司要求我们开发者甚至要支持 IE11。他们基本上希望几乎每个人都能获得相同的浏览体验。
我喜欢 CSS 变量和主题化的概念,但是否有办法使用它或类似的东西,或者我是否必须为 IE 提供回退,从而失去其意义?
我很想听到更多关于这个主题的内容。
我不知道这是否对您有帮助,Jake,但是您可以将此概念与 SASS 一起使用,这应该能让您在 IE11 的范围内工作。
嗨,Jake,虽然作者在这里专门使用了 CSS 变量,但您会注意到,他们说过这些概念也适用于 LESS、SASS 或 PostCSS 等预处理器。 :)
使用 SCSS (Sass)。我认为 Sass 不会很快消失,所以值得投资。
除非您希望能够实时自定义前端主题,否则您不一定需要 css 变量。相同的概念可以应用于任何具有变量的 css 预处理器,例如 SASS 和 LESS。整个想法是使用变量来帮助保持主题的一致性。
使用 PostCSS 和http://cssnext.io怎么样?在我看来,它可以让您甚至在 ie9 中使用 css 变量。这种方法只需要在编译 CSS 时比原生 CSS 变量多一步。但允许使用上述所有技术。
如果您不想使用 SASS 或任何其他预处理器,请改用 PostCSS。例如 CSSNext – http://cssnext.io/features/#custom-properties-var
感谢大家关于 IE 支持的回复。我刚刚开始学习如何使用 SASS,因为我的雇主已经讨论过我们开始使用它。我还将看看 Scott、Pavel 和 basher 都建议的 PostCSS。我刚刚查看了 cssnext 页面,它看起来也很有趣。
谢谢!
太棒了!快速提问:为什么回退值不在组件变量定义处,而是在使用它们的地方?
最初,我们在尝试声明自定义属性以及使用另一个具有默认值的自定义属性时遇到了一些浏览器支持方面的问题。不过,我刚刚做了一个快速检查,无法重现这个问题,所以这可能是一个好方法,但我建议您先确认您的目标浏览器是否能很好地支持它。
Pavel,我完全同意您的观点。我在https://webdesignblog.info/上意识到了这种方法。
没有这个,我就无法支持 ie9。CSSnext 解决了这个问题。
设置 CSS 变量以对设计系统进行主题化可能会很棘手:如果它们的范围太小,系统将失去一致性。如果它们的范围太大,您将失去粒度。
https://run3coolmath.io/