Writing

使用 Style Dictionary 将设计标签转换成 CSS 的完整指南

文章發表於

本文将介绍《设计标签》 与 CSS 之间的关系以及如何通过 Style Dictionary 将设计标签转换成 CSS。

为什么需要 Style Dictionary?

在 Style Dictionary 出现之前,各平台的工程师往往各自建立一套管理系统来维护设计团队定义的视觉元素。例如:

  • 前端工程师可能使用 Sass/SCSS 来管理样式变量
  • iOS 工程师可能使用 Swift 来定义视觉元素
  • Android工程师则可能采用XML资源文件

然而,各平台定义颜色的方式不尽相同,例如 iOS 使用 RGB 十进制来定义颜色,而 Android 则是使用十六进制来定义颜色,这种情况下,设计师在定义视觉元素时,就必须考虑到不同平台的差异,从上至设计标签到下至组件的管理,一旦某个环节出错,就可能造成不同平台的视觉不一致的情况。

什么是 Style Dictionary?

Style Dictionary 是一个由 Amazon 所开源的工具,可以通过它管理设计标签,并且可以生成各种平台的 Styling,例如 CSS、SCSS、JS、Swift、Android 等等。

除此之外 Style Dictionary 可以进行定制化的需求,让我们可以更灵活地管理设计标签。

如何通过 Style Dictionary 来管理设计标签?

许多大厂的设计团队通常会将设计系统里所需要的视觉元素定义在 Figma,例如颜色、字体、间距等等。

而前端工程师们可以将这些视觉元素以键值对(Key-Value Pair) 的方式来管理,并且通过 Style Dictionary 来将其进行转换成不同平台的 Styling。

假设现在我们的 Styling 只有 color-primary

{
"color": {
"red": {
"100": {
"type": "color",
"value": "#FFCDD2"
},
"200": {
"type": "color",
"value": "#EF9A9A"
}
}
}
}

以 Web 来说,我们就可以通过 CLI 工具,生成 CSS, SCSS 或是 JS 的文件,之后在引入到项目中,就可以使用这些变量来进行开发。

CSS:

:root {
--ui-color-red-100: #ffcdd2;
--ui-color-red-200: #ef9a9a;
}

JS:

export const ColorRed100 = '#ffcdd2'
export const ColorRed200 = '#ef9a9a'

Style Dictionary Playground

CTI (Category/Type/Item)

《设计标签》这篇文章提到,设计标签不只有最上层的 Reference Token 也有像是 System Token 跟 Component Token,这种分类方式我们称 CTI,也正是 Style Dictionary 所建议的分类方式。而 System Token 跟 Component Token 就是参照 Reference Token 来定义。

举例来说 color-primary 就可通过参照 color.red.200color.red.100 来定义。

{
"color": {
"primary": {
"dark": {
"value": "{color.red.200}"
},
"light": {
"value": "{color.red.100}"
}
}
}
}

而生成出来的 CSS 就会是这样:

:root {
--ui-color-primary-dark: #ef9a9a;
--ui-color-primary-light: #ffcdd2;
}

建立设计标签

了解什么是 Style Dictionary 以及如何使用它来管理视觉元素后,接下来将介绍如何实践从设计标签到 CSS 的流程。

  1. 建立一个新的项目,项目名称为 design-tokens
  2. 了解视觉元素的结构,本文会以 Material Design 当作基底。
  3. 将视觉元素 (color, space 以及 shadow 等等) 转换成键值对(Key-Value Pair) 的 JSON 文件。
  4. 最后在通过 Style Dictionary 来生成不同平台的 Styling (本文会以 Web 的 CSS 作为范例)。

项目目录

本文将在现有的设计系统中新增 design-tokens 套件,而以下是从系列的第一篇文章至今,完整的项目结构:

design-system/
├── README.md
├── package.json
├── pnpm-lock.yaml
└── packages/
├── components/
│ ├── focus-scope/
│ └── visually-hidden/
├── design-tokens/ # <-- 新增的设计标签套件
└── tsconfig/
...

Step.1 - 初始化 design-tokens

建立 packages.json

首先在 packages 下新增一个 design-tokens 的文件夹,并且初始化 packages.json。

design-system > mkdir design-tokens
design-system > cd design-tokens
design-system > pnpm init

安装相关套件

design-tokens 项目下安装以下套件:

  • style-dictionary:设计标签管理核心工具
  • sass:CSS 预处理器,提供更有组织性的样式开发方式
  • stylelint:CSS 代码质量检查工具
  • 其他:Stylelint 相关设置文件
pnpm add style-dictionary sass postcss stylelint stylelint-config-prettier stylelint-config-standard stylelint-config-standard-scss stylelint-order stylelint-prettier -D

设置 Stylelint

design-tokens 的根目录底下建立 stylelintrc.json

design-system > touch stylelintrc.json

可以根据喜好设置 Stylelint,这是该项目的设置档

Step.2 - 将视觉元素转换成设计标签

Material Design 将设计标签切分成三个层级,分别为 Reference Token, System Token 以及 Component Token。

Step.3 - 管理设计标签

由于 Material Design 将 Token 切分成三个层级,第一个问题就是要先定义哪一层级的 Token 需要纳入 design-tokens 这个 packages 中管理?

在目前的架构中,我们将 Reference Token 与 System Token 纳入 design-tokens,而 Component Token 则是放在各个组件中。这也是现今许多大厂设计系统的做法,如 Pinterest 的 Gestalt 或是 Shopify 的 Polaris 等等。

视觉元素转化成 JSON

接下来要将 Material Design 的视觉元素转换成键值对(Key-Value Pair) 的形式,这里会以 color 作为范例,而以下是 design-tokens 的结构:

design-tokens
├── README.md
├── package.json
├── pnpm-lock.yaml
├── tokens
│ ├── color
│ | ├── base.json <--- Reference Token
│ | ├── alias.json <-- System Token
...

我们会将 Reference Token 与 System Token 分别存放在 base.jsonalias.json,且 alias.json 内的值则会参照 base.json

这也呼应了前面所提到的一致性,当公司想要品牌重塑时,就只需要更改 Reference Token 的值,而不需要更改所有层级的设计标签。

Reference Token - base.json

在 Material Design 中 (如上图),可以看到所有 Reference Token 这是最上层也是最抽象的,而将其转换成 Token 就会如下:

{
"ref": {
"palette": {
"primary": {
"0": { "value": "#000000" },
"10": { "value": "#21005D" },
...
"100": { "value": "#FFFFFF" }
},
"secondary": {
"0": { "value": "#000000" },
"10": { "value": "#1D192B" },
...
"100": { "value": "#FFFFFF" }
},
}
}
}

System Token - alias.json

System Token 相较于 Reference Token 会有更明确的定义,例如 --sys-color-primary-container 可以清楚知道这是 primary 的颜色,且是用在 container 上。

大部分的 System Token 其背后都会有对应的 Reference Token,在 color 中便是如此:

{
"sys": {
"color": {
"primary": {
"light": { "value": "{ref.palette.primary.40.value}" },
},
"primary-container": {
"light": { "value": "{ref.palette.primary.90.value}" },
}
}
...
}
}

也可以定义该 System Token 在深色模式 (Dark Mode) 中的值

{
"sys": {
"color": {
"primary": {
"light": { "value": "{ref.palette.primary.40.value}" },
"dark": { "value": "{ref.palette.primary.80.value}" }
},
"primary-container": {
"light": { "value": "{ref.palette.primary.90.value}" },
"dark": { "value": "{ref.palette.primary.30.value}" }
}
}
...
}
}

当然视觉元素不会只有颜色,也会有字体、间距等等,其概念都是一样的,这里就不多做赘述,可以参考 design-tokens 的完整设置。

Step.4 - 通过 Style Dictionary 来生成不同平台的 Styling

最后在通过 Style Dictionary 来生成各平台的 Styling,而其会需要一个 config.js 来定义所有构建逻辑。其还有另一个优点就是可以通过一些定制化的 script 来对其进行扩充。

建立 config.js

{
"source": ['./tokens/**/*.json'],
"platforms": {
"css": {
"transformGroup": "css",
"buildPath": "dist/css/",
"files": [
{
"destination": "_variables.css",
"format": "css/variables"
}
]
},
"js": {
"transformGroup": "js",
"buildPath": "dist/js/",
"files": [
{
"destination": "variables.js",
"format": "javascript/es6"
}
]
}
}
}

首先 source 是用来定义要转换的文件,像是上面的例子,我们会将所有的 token 都放在 tokens 底下,故设置为 ./tokens/**/*.json。接下来就是 platforms,这里会定义要转换成哪些平台,像是 CSS、JS 等等,而每个平台都会有 transformGroupfiles,可以参考 Style Dictionary - Architecture 的文档。

以下是该项目的设置:

  1. 通过 build.js 将 config.json 做延伸,并且定义对应的 transformGroupfiles
  2. 再来当 Style Dictionary 将先前定义好的 Token 转换成 CSS 后,我们可以通过 SASS 建立 CSS 基底,并将所有 CSS 引入 normalized.scss 作为入口,在打包时建立出 dist/normalize/normalize.css

这个好处是可以把一些基本的 CSS 与设计标签的 CSS 整理成一个文件,而在引用的时候就可以直接引用 normalize.css。也可以通过以下方式来将其引用到 Storybook 中。

// .storybook/preview-head.html
<link
href="https://cdn.jsdelivr.net/npm/@tocino-ui/design-tokens/dist/normalize/normalize.css"
rel="stylesheet"
/>

小结

最后,有了属于自己 Design System 的 normalize.css 之后,就可以在项目中引入它,并且使用 Design Token 来进行开发了!

请点击『展开代码』以查看代码。
import { useEffect, useState, useRef } from 'react'
import './styles.css'

export default function App() {
  const [theme, setTheme] = useState('light')

  useEffect(() => {
    document.documentElement.setAttribute('data-theme', theme)
  }, [theme])

  return (
    <div className="App">
      <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/@tocino-ui/design-tokens/dist/normalize/normalize.css"
      />
      <h1>Hello World!!</h1>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle Theme</button>
      <p> 可以尝试将点击 Toggle Theme 来切换主题,并且观察 `data-theme` 的变化。</p>
    </div>
  )
}

如果您喜欢这篇文章,请点击下方按钮分享给更多人,这将是对笔者创作的最大支持和鼓励。
Buy me a coffee