Tailwind 4中的类型安全设计令牌探索
Tailwind 4 已经计划了一段时间,团队在 2024 年 3 月 开源了他们的进展。最值得注意的变化之一是从基于 JavaScript 的配置转向了基于 CSS 的配置,在我看来。目前 Tailwind 4 正在测试版阶段,据我所知,团队仍在解决一些问题,特别是在 Safari 兼容性方面。
注:稍后在文章中,我们将假设您正在使用基于组件的框架/库,但所讨论的概念可以轻松应用于其他方法或框架,这样更流畅且含义更明确。
尾风 4 的更新迁移到 CSS 配置文件。
我听说有些人抱怨,尤其是来自 TypeScript 用户的抱怨。但是,Tailwind 4.0 的路线图将优先支持对经典的 tailwind.config.js
,作为其首要任务。
支持 JavaScript 配置文件 — 重新支持经典的 tailwind.config.js 文件,以使迁移到 v4 更加简单。
话说,这主要是为了迁移目的,并可能不是一个长期可行的解决方案。
这对类型安全来说意味着什么
Tailwind 4 使用新的 @property
CSS 规则来定义内部自定义属性。
我们用 @property
来定义内部自定义的属性,带有适当类型和约束。
目前我无法在 VS code 中找到合适的 @property
规则语法高亮功能,我在 Bluesky 上发帖询问是否有人在这方面做得更好,看看是否有更好的解决方案。
我希望更好的 @property
支持能在将来帮助我们,关于这一点,我们以后再聊。
@属性 CSS 规则是什么意思
什么是设计元素(tokens)?
@property
规则允许直接在样式表中注册自定义属性,而无需运行任何 JavaScript 代码。有效的@property
规则将注册一个自定义属性,这和使用registerProperty()
函数来注册自定义属性类似。
现在我们已经介绍了 Tailwind 4 即将带来的变化及其可能的影响,让我们花点时间谈谈设计令牌(设计令牌是存储和管理设计决策的一种方法)。如果你不熟悉这个术语,这里有一个快速解释:设计令牌是一种以一致和可重用的格式存储和管理设计决策的方法。它们代表设计系统的关键视觉属性,如颜色、字体、间距、阴影等,并以结构化方式保存。目标是把这些设计值集中在一处,以便它们可以在不同的平台和工具之间轻松更新、维护和共享。
设计系统通常包含两种主要类型的值:系统值
和 组件值
。例如:你的系统值可能如下所示:
const SYSTEM_TOKENS: ISystemTokens = {
/* ... */
COLORS: {
/* ... */
GREEN: {
LIGHT: "#E0E5D9",
中等色: "#3F6212",
深绿色: "#28331A",
}
/* ... */
},
TYPOGRAPHY: {
/* ... */
}
/* ... */
}
全屏模式。退出全屏。
你可以在组件里的标记里引用系统值,比如这样:
import { SYSTEM_TOKENS } from "...";
const 按钮属性: IButtonTokens = {
/* ... */
颜色: {
/* ... */
背景: SYSTEM_TOKENS.颜色.绿色.深色,
/* ... */
},
排版: {
/* ... */
}
/* ... */
}
全屏模式 退出全屏
如果你对设计系统感兴趣的话,可以试试了解一下知名的系统,比如[Material Design(材料设计)],https://m3.material.io/foundations/design-tokens/overview。
使用 Tailwind CSS 设计组件令牌大约一周前,我写了一篇文章,讨论了用 Tailwind CSS 创建组件变体的另一种方法。简而言之,这篇文章探讨了如何利用 CSS 变量与 Tailwind 来处理复杂变体,通过动态组件属性和变量映射直接设置变体值。如果你想了解我是如何想到这种方法的,可以在这里阅读更多内容:用 Tailwind CSS 写组件变体的不同方法。
我们应该从识别依赖设计令牌的部分开始。正如我们之前提到的,这将包括颜色、字体、间距以及其他任何对您的设计至关重要的固定系统值。我们来看一下这个不使用设计令牌的Button 组件
:
<button class="p-4 bg-red text-white rounded-lg relative flex justify-center">点我</button>
进入全屏 退出全屏
在上面的例子中,我们可以识别出几个可以被分词化或标记化的值。以下每一个类都可能对应于我们设计体系中的一个值:
- p-4 边距为4个单位
- bg-red 背景为红色
- text-white 文本颜色为白色
既然我们已经确定了可以分词的值,我们可以将它们分为两类:静态值
和动态值
。静态值是指在组件中保持不变的值,而动态值是指可以通过传递给组件的属性来变化的值。例如,在我们的示例中,我们将使内边距(p-4
)保持不变,而文本颜色(text-white
)和背景(bg-red
)则应通过theme
属性动态设置。
生成令牌
Tailwind 4框架配置文件
首先,我们需要在新 Tailwind CSS 配置文件中定义 System tokens
:
@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;
}
全屏模式,退出全屏
系统 token (系统令牌)
接下来我们需要创建名为 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-medium";
DARK: "--color-green-dark";
},
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>.tokens.ts
)。为了说明这一点,我们来看一个例子,让我们以之前讨论的 Button 组件为例,创建一个对应的 Button.tokens.ts
文件。
概括来说,我们的 Button 组件结构如下:
<button class="p-4 bg-red text-white rounded-lg relative flex justify-center" aria-label="点我">点我</button>
全屏模式:点击这里可以进入/退出全屏
之前,我们讨论了静态值(如 p-4
)和动态值(如 bg-red
和 text-white
)的区别。这种区别将指导我们如何组织设计令牌。静态属性,如 p-4
,应放在 STATIC
下,而动态属性,如 bg-red
和 text-white
,应根据其属性标识符分组。在这种情况下,由于我们通过 theme
属性来设置 bg-red
和 text-white
,因此,它们应该放在标记文件的 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: IButtonThemeTokens = {
PRIMARY: {
backgroundColor: "--color-red-dark",
textColor: "--color-red-light",
},
SECONDARY: {
backgroundColor: "--color-green-dark",
textColor: "--color-green-light",
},
};
全屏 退出全屏
组件设计标记可以在设计中像这样引用:$component.Button.THEME.PRIMARY.backgroundColor
。我更喜欢这样的命名约定:
$component.组件名.属性值.tokenName
$component
:区分 $system
和 $component
代号
<ComponentName>
:遵循组件的内部文件命名约定
PROP_NAME
:属性名称应使用常量命名
PROP_VALUE
:属性值应遵循内部命名约定
Token 名称 (如 backgroundColor)
:应使用驼峰命名*
这完全是个人喜好的问题,你来决定什么最适合你的工作流程。
- 命名令牌时,如果需要为元素的状态(如
:hover
)指定一个令牌,我会稍微偏离驼峰命名规则。在这种情况下,我通常会在令牌名称前加上状态,后面跟两个下划线,例如:hover__背景颜色
。
在组件中使用设计 token
正如我在前面的文章中提到的,我之前写过一篇关于用 Tailwind CSS 编写组件变体样式的另一种方法的文章(探索一种用 Tailwind CSS 编写组件变体样式的替代方法)。本文会引用那篇文章中的方法,如果你还没看过,建议先读一下,这有助于你理解这种方法的背景。
在这里假设你使用一个JavaScript框架或库来创建组件。
更新一下 Button 组件
我们需要用由css变量支持的Tailwind类来替换现有的标记类。请注意,变量名称与我们按钮组件中的两个标记接口 IButtonStaticTokens
接口和 IButtonThemeTokens
接口中的名称相匹配;
<button class="p-[--padding] bg-[--backgroundColor] text-[--textColor] rounded-lg relative flex justify-center">点我</button>
进入全屏模式
退出全屏模式
更新了类之后,我们现在需要动态地应用组件样式并更新相关变量。为此,我们将在组件上使用一个 variableMap
函数。该函数将我们从 Button.tokens.ts
中的 token 映射到组件上的内联 CSS 变量,这样我们的类就可以直接引用这些变量了。有关变量映射的示例请参阅,文章的末尾有更多相关信息,请参阅 这篇文章。
<template>
<button
:style="[variableMap(STATIC), variableMap(THEME[props.THEME])]"
class="p-[--padding] bg-[--backgroundColor] text-[--textColor]
rounded-lg relative flex justify-center"
>
点击这里
</button>
</template>
<script setup lang="ts">
import { variableMap } from "...";
import { STATIC, THEME } from "Button.tokens.ts";
const props = /*THEME*/
</script>
全屏 退出全屏
最后我们来总结一下我期待 Tailwind 4 的发布,看看团队在这段时间里会做出哪些改动。我一直享受尝试各种方法来解决设计令牌、变体和类型安全方面的挑战。
这是一个实验性的方法,肯定会引发一些强烈的反馈。
共同学习,写下你的评论
评论加载中...
作者其他优质文章