探索 Tailwind 4 中的 Typesafe 设计令牌
Tailwind 4 的开发已近在眼前,团队于 2024 年 3 月首次开源了其开发进度。在我看来,最值得关注的变化之一是从基于 JavaScript 的配置转变为基于 CSS 的配置。Tailwind 4 目前处于测试阶段,据我了解,团队仍在应对一些挑战,尤其是在 Safari 兼容性方面。
注意:在本文的后面,我们将假设您正在使用基于组件的框架/库,但所讨论的概念很容易转移到其他方法。
Tailwind 4 的变化
转向 CSS 配置
我听到过一些关于这一点的抱怨,尤其是来自 TypeScript 用户的抱怨。不过,Tailwind 4.0 的路线图确实将对经典版本的支持列为tailwind.config.js
首要任务:
支持 JavaScript 配置文件——重新引入与经典 tailwind.config.js 文件的兼容性,以便轻松迁移到 v4。
话虽如此,但这似乎主要是为了迁移目的,可能不是一个可持续的长期解决方案。
这对类型安全意味着什么
在底层,Tailwind 4 使用新的@property
CSS 规则来定义内部自定义属性。
我们用
@property
适当的类型和约束来定义我们的内部自定义属性
目前,我无法@property
在 VS 代码中找到对规则的体面语法高亮支持,我已经联系了Bluesky,看看是否有人比我更幸运。
我希望@property
将来能有更好的支持来帮助我们,稍后我会详细介绍。
什么是 @property CSS 规则
该
@property
规则表示直接在样式表中注册自定义属性,无需运行任何 JavaScript。有效的 @property 规则会注册自定义属性,类似于registerProperty()
使用等效参数进行调用。
什么是设计令牌
既然我们已经介绍了 Tailwind 4 即将推出的变更及其潜在影响,那么接下来让我们来聊聊设计令牌 (Design token)。如果您不熟悉这个术语,这里有一个简短的解释:设计令牌是一种以一致、可重复使用的格式(通常作为变量)存储和管理设计决策的方法。它们以结构化的方式表示设计系统的关键视觉属性,例如颜色、排版、间距、阴影。目标是将这些设计值集中起来,以便它们能够在不同的平台和工具之间轻松更新、维护和共享。
设计系统通常由两种主要类型的值组成:System values
和Component values
。例如,您的系统值可能如下所示:
const SYSTEM_TOKENS: ISystemTokens = {
/* ... */
COLORS: {
/* ... */
GREEN: {
LIGHT: "#E0E5D9",
MEDIUM: "#3F6212",
DARK: "#28331A",
}
/* ... */
},
TYPOGRAPHY: {
/* ... */
}
/* ... */
}
然后,您可以在组件令牌中引用系统值,如下所示:
import { SYSTEM_TOKENS } from "...";
const BUTTON_VALUES: IButtonTokens = {
/* ... */
COLORS: {
/* ... */
BACKGROUND: SYSTEM_TOKENS.COLORS.GREEN.DARK,
/* ... */
},
TYPOGRAPHY: {
/* ... */
}
/* ... */
}
如果您有兴趣了解有关设计系统的更多信息,那么值得探索一下像Material Design这样的知名系统。
使用 Tailwind 构建组件设计令牌
大约一周前,我写了一篇文章,讨论了我使用 Tailwind CSS 创建组件变体的另一种方法。简而言之,本文探讨了如何结合使用 CSS 变量和 Tailwind 来管理复杂的变体,并通过动态组件 props 和变量映射在内联中设置变体值。如果您对我如何想到这种方法感到好奇,可以在这里阅读更多相关信息:使用 Tailwind CSS 编写组件变体的另一种方法。
我们应该首先确定组件中依赖于设计令牌的部分。如前所述,这将包括颜色、字体、间距以及设计中不可或缺的任何其他固定系统值。让我们看一下以下不依赖设计Button component
令牌的情况:
<button class="p-4 bg-red text-white rounded-lg relative flex justify-center">Click me</button>
在上面的例子中,我们可以精确地指出几个可以标记的值。以下每个类都可以对应我们设计系统中的一个值:
- p-4
- bg-红色
- 文本白色
现在我们已经确定了可以标记的值,我们可以将它们分为两类:static values
和dynamic
值。静态值是组件中保持不变的值,而动态值是可以根据传递给组件的 props 而变化的值。在我们的示例中,我们将 padding ( p-4
) 设为静态,而文本颜色 ( text-white
) 和背景 ( bg-red
) 则应通过 props 动态设置theme
。
创建代币
Tailwind 4 配置
首先,我们需要System tokens
在新的 Tailwind CSS 配置中定义:
@import "tailwindcss";
@theme {
--color-white: #FFFFFF;
--color-green-light: #E0E5D9;
--color-green-medium: #3F6212;
--color-green-dark: #28331A;
--color-red-light: #F4CCCC;
--color-red-medium: #D50000;
--color-red-dark: #640000;
--spacing-sm: 1rem;
--spacing-md: 2rem;
}
系统代币
接下来我们需要创建system.tokens.ts
文件:
export type TColor = "--color-white" | "--color-green-light" | "--color-green-medium" | "--color-green-dark" | "--color-red-light" | "--color-red-medium" | "--color-red-dark";
export type TSpacing = "--spacing-sm" | "--spacing-md";
interface ISystemTokens {
COLORS: {
WHITE: TColor;
GREEN: {
LIGHT: TColor;
MEDIUM: TColor;
DARK: TColor;
},
RED: {
LIGHT: TColor;
MEDIUM: TColor;
DARK: TColor;
}
},
SPACING: {
SMALL: TSpacing;
MEDIUM: TSpacing;
}
}
export const SYSTEM_TOKENS: ISystemTokens {
COLORS: {
WHITE: "--color-white";
GREEN: {
LIGHT: "--color-green-light";
MEDIUM: "--color-green-light";
DARK: "--color-green-light";
},
RED: {
LIGHT: "--color-red-light";
MEDIUM: "--color-red-medium";
DARK: "--color-red-dark";
}
},
SPACING: {
SMALL: "--spacing-sm";
MEDIUM: "--spacing-md";
}
}
系统设计标记可以在如下设计中引用:$system.COLORS.GREEN.LIGHT
。
理想情况下,我们可以直接从 CSS 文件的 @property 规则中导出类型到我们的TColor
和TSpacing
类型中,就像 SCSS 导入可以转换为 JavaScript 一样。不幸的是,据我所知,目前这还无法实现。
组件标记
现在我们已经实现了系统令牌,可以开始将它们集成到组件中了。第一步是设置<Component>.tokens.ts
文件。为了说明这一点,我们以之前查看过的 Button 组件为例,创建一个相应的Button.tokens.ts file
。
回顾一下,我们的 Button 组件的结构如下:
<button class="p-4 bg-red text-white rounded-lg relative flex justify-center">Click me</button>
之前,我们讨论了静态值(如p-4
)和动态值(如bg-red
和text-white
)之间的区别。这种区别将指导我们如何组织设计 token。静态属性(如p-4
)应归入STATIC
,而动态属性(如bg-red
和text-white
)应归入相应的 prop 标识符。在本例中,由于我们通过prop 控制bg-red
和,因此它们应放在tokens 文件中的 部分下。在我们的示例中,我们假设有两个主题变量 -和。text-white
theme
THEME
PRIMARY
SECONDARY
import { SYSTEM_TOKENS, TColor, TSpacing } from "./system.tokens.ts";
import { TTheme } from "./Button"; // PRIMARY, SECONDARY
interface IButtonStaticTokens {
padding: TSpacing;
}
interface IButtonThemeTokens {
backgroundColor: TColor;
textColor: TColor;
}
export const STATIC: IButtonStaticTokens {
padding: "--spacing-sm";
}
export const THEME: IButtonStaticTokens {
PRIMARY: {
backgroundColor: "--color-red-dark";
textColor: "--color-red-light";
},
SECONDARY: {
backgroundColor: "--color-green-dark";
textColor: "--color-green-light";
};
}
组件设计标记可以在设计中引用,如下所示:$component.Button.THEME.PRIMARY.backgroundColor
。我倾向于使用以下命名约定:$component.<ComponentName>.<PROP_NAME><PROP_VALUE>.tokenName
$component
:区分$system
和$component
标记<ComponentName>
:遵循组件的内部文件命名约定PROP_NAME
:常量大小写PROP_VALUE
:应遵循内部 prop 值大小写Token name (backgroundColor)
:驼峰式大小写*
这最终是一个个人喜好的问题,由您来决定什么最适合您的工作流程。
- 在命名 token 时,如果需要为元素的状态指定 token,例如 ,我会稍微偏离驼峰命名法
:hover
。在这种情况下,我会在 token 名称前加上状态前缀,后跟两个下划线,如下所示:hover__backgroundColor
。
在组件中使用设计令牌
正如我在文章前面提到的,我之前写过一篇关于探索使用 Tailwind CSS 编写组件变体的不同方法的文章。我将在这里引用该方法,所以如果你还没有读过,最好先阅读一下,以了解该方法背后的背景。
本文的这一部分将假设您使用 Javascript 框架或库来构建组件。
更新按钮组件
我们需要用由 CSS 变量支持的 Tailwind 类替换现有的可标记类。请注意,变量名称与我们的两个 Button 组件标记接口中的变量名称一致,IButtonStaticTokens
并且IButtonThemeTokens
;
<button class="p-[--padding] bg-[--backgroundColor] text-[--textColor] rounded-lg relative flex justify-center">Click me</button>
现在我们已经更新了类,我们需要动态地应用组件样式并更新变量。为此,我们将variableMap
在组件上使用一个函数。本质上,这个函数将我们的标记映射到组件上的内联 CSS 变量,然后我们的类就可以引用这些变量。有关变量映射的示例,请参阅本文Button.tokens.ts
末尾。
<template>
<button
:style="[variableMap(STATIC), variableMap(THEME[props.THEME])]"
class="p-[--padding] bg-[--backgroundColor] text-[--textColor]
rounded-lg relative flex justify-center"
>
Click me
</button>
</template>
<script setup lang="ts">
import { variableMap } from "...";
import { STATIC, THEME } from "Button.tokens.ts";
const props = /*THEME*/
</script>
结论
我期待着 Tailwind 4 的发布,以及团队在此期间做出的改进。我很享受尝试各种想法来应对设计令牌、变体和类型安全方面的挑战。
这是一种实验性的方法,我相信会有一些强烈的意见。
如果您发现这篇文章有趣或有用,请在Bluesky(我在这里最活跃)、Medium、Dev和/或Twitter上关注我。
文章来源:https://dev.to/wearethirdbears/exploring-typesafe-design-tokens-in-tailwind-4-372d