注意
本页将提供 TailwindCSS (v4) 等原子类样式库与 Semi 共同使用时遇到的一些问题的最佳实践。
这些问题在其他组件库与 Tailwind 共同使用时候也会经常遇到,但 Semi 提供了官方解决方案,建议按照本文说明,正确配置项目。
1. 解决样式覆盖优先级问题
问题表现
在组件中使用部分原子类时没有效果 或 组件库样式异常。
原因分析
使用 Tailwind 时,Tailwind 通过类名对 dom 进行样式修改,同样作用于 Semi 组件库。同时 Tailwind 默认开启了 Preflight 来重置浏览器默认样式。
没有任何层声明时,只会出现两种结果,取决于项目的配置和入口的 import 顺序:
- Tailwind 比 Semi 的样式先引入,Semi 优先级更高
- Semi 样式 比 Tailwind 先引入,Tailwind 优先级更高
如果是 1,则会出现 Tailwind 在添加某些原子类时,如果组件样式已经定义了某个 css 属性,原子类的优先级比 Semi 优先级低,此时原子类失效。例如在 1 的前提下,对 Button 组件设置 padding,会出现失效的情况。
如果是 2,因为 Tailwind 优先级较高,其对浏览器默认样式覆盖的 Preflight 会同时覆盖掉 Semi 的样式。例如在 2 的前提下,light 的 Button 的背景色会被覆盖为 transparent,导致样式表现异常。
解决方案
无论 Tailwind 和 组件库哪一方优先级高,都会出现问题,因此解决方式在于正确处理 Tailwind 样式中 Preflight 和用户需要的原子类的优先级相对于组件库优先级的关系。声明层顺序是集成的前提,不是可选优化。
1. Semi 样式默认位于 semi Layer 中
Semi 发布的所有样式都包裹在 @layer semi { ... } 中:各框架组件库(如 @fibrous/semi-ui-react)中编译好的 CSS(包括 styles.css),以及 Semi 主题插件(@fibrous/semi-webpack-plugin、@fibrous/semi-rspack-plugin、@fibrous/semi-vite-plugin、@fibrous/semi-next)的产物,插件的 cssLayer 选项默认为 true。与 Tailwind 同用时保持默认,不要传入 cssLayer: false,否则 Semi 的样式不再受层顺序控制,会回到上面的两种结果。
2. 声明层顺序
- 创建一个 CSS 文件(如
semi-layer.css),内容如下,五层都要列出:
@layer theme, base, semi, components, utilities;
semi 必须位于 base 之后、components 与 utilities 之前:Preflight(base)压不住 Semi,工具类又能覆盖 Semi。漏写 components 会让 Tailwind 自己的 components 层排到 utilities 之后,破坏 Tailwind 的默认约定。
- 在项目的 JS 入口文件(如
main.ts、main.tsx或index.js)的最顶部,在所有其他 import 语句之前,import 这个文件:
// main.tsx 或 index.js - 必须放在最顶部
import './semi-layer.css'; // 必须在最前面
import './tailwind.css'; // 包含 @import "tailwindcss"; 的文件
import { Button } from '@fibrous/semi-ui-react';
// ... 其他 import
服务端渲染框架没有这样的 JS 入口,两个 import 放在根组件文件的最顶部:Next.js App Router 为 app/layout.jsx,React Router 为 app/root.tsx。根组件先于所有页面加载,开发与生产环境下服务端渲染的首屏样式都以层顺序声明开头。
// app/layout.jsx(Next.js App Router)或 app/root.tsx(React Router),放在最顶部
import './semi-layer.css';
import './tailwind.css';
Tailwind 按其官方指南接入构建:Next.js 用 @tailwindcss/postcss,React Router 用 @tailwindcss/vite,与 Semi 的 Vite 插件并列。
// postcss.config.mjs,Next.js
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};
// vite.config.ts,React Router
import { reactRouter } from '@react-router/dev/vite';
import semi from '@fibrous/semi-vite-plugin';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), semi(), reactRouter()],
});
原理
通过 CSS Layer 特性,实现不同来源的样式的优先级设置。所有的 Semi 样式都包裹在 @layer semi { ... } 中,上面的声明设置了各层的优先级顺序:
@layer theme, base, semi, components, utilities;
含义为:base(含 Preflight)优先级最低,Semi 次之,Tailwind 的 components 与用户设置的原子类(p-0 等)优先级最高。后声明的层优先级更高,与选择器权重无关,因此单个类名的工具类就能覆盖 Semi 的根元素样式,不需要 !important。
2. 解决在 Tailwind 原子类中使用 Semi Token 的问题 (可选)
Tailwind 支持用户配置自己的 Token 来实现主题。同时 Semi 也提供了自己的主题方案与对应 Token 如果想在项目中直接使用 Semi Token,例如将一个 span 的文字颜色设置为 --semi-color-text-0 来实现亮暗色的颜色自动切换与主题保持一致,需要单独设置 css color: var(--semi-color-text-0),很不方便。
Semi 在构建时从默认主题生成 Tailwind 的主题映射文件 tailwind.css,把语义层 Token 与调色板映射为原子类 Token,不需要手抄。在包含 @import "tailwindcss"; 的 CSS 文件中引入:
@import 'tailwindcss';
@import '@fibrous/semi-ui-react/tailwind.css';
之后即可使用 bg-semi-color-primary、text-semi-color-text-0 等原子类,也支持透明度修饰符:
<div className="bg-semi-color-primary/50 text-semi-color-text-0 rounded-semi-large shadow-semi-elevated">...</div>
映射文件使用 @theme inline,只写 var(--semi-*) 引用,所以原子类跟随当前主题和暗色模式取值,在 .semi-always-light、.semi-always-dark 子树内也按元素所处位置解析。命名规则:
| Semi Token | Tailwind Token | 原子类示例 |
|---|---|---|
--semi-color-*(非渐变) | --color-semi-color-* | bg-semi-color-primary |
调色板 --semi-<色名>-<色阶> | --color-semi-<色名>-<色阶> | bg-semi-blue-5 |
--semi-border-radius-* | --radius-semi-* | rounded-semi-large |
--semi-shadow-* | --shadow-semi-* | shadow-semi-elevated |
--semi-transition_function-* | --ease-semi-* | ease-semi-easeIn |
--semi-spacing-* | --spacing-semi-* | p-semi-base、gap-semi-tight |
--semi-font-size-* | --text-semi-* | text-semi-regular |
--semi-line-height-* | --text-semi-*--line-height | 随 text-semi-* 一起设置 |
--semi-font-weight-* | --font-weight-semi-* | font-semi-bold |
--semi-font-family-* | --font-semi-family-* | font-semi-family-regular |
text-semi-regular 同时设置 Semi 配对的字号与行高(14px/20px)。字体族带 family- 前缀,因为 Tailwind 的 font-* 同时读取字体族与字重两个命名空间,不加前缀时 font-semi-regular 会同时指向常规字体族和常规字重。
<div className="p-semi-base gap-semi-tight text-semi-header-6 font-semi-bold">...</div>
z-index、控件高度、描边宽度在 Tailwind 中没有对应的命名空间,不映射,用任意值写法:
<div className="z-(--semi-z-modal) h-(--semi-height-control-default) border-(length:--semi-border-thickness-control)">...</div>
调色板共 212 个颜色:16 个色系各 0 到 9 共 10 个色阶(如 semi-blue-5、semi-light-green-2),semi-white、semi-black,AI 紫色 semi-ai-purple-0 到 semi-ai-purple-9,以及 AI 渐变的色标 semi-ai-general-<色阶>-<0 到 3>。调色板的值是完整颜色,暗色模式下同名色阶换成暗色版本,同样支持透明度修饰符:
<div className="bg-semi-blue-0 text-semi-blue-7 border-semi-grey-2/50">...</div>
AI 场景的渐变色(--semi-color-ai-general*、--semi-ai-general-<色阶>)、动画时长等其余 Token 不映射,需要时同样用任意值写法,例如 bg-(image:--semi-color-ai-general)。
映射文件按默认主题生成。使用其他主题包时,间距、字号、行高、字重、字体族这几类原子类只有在该主题的 global.scss 也声明了同名 --semi-* 变量时才有值。
暗色变体 dark:
Semi 的暗色开关是 theme-mode 属性,而不是 Tailwind 默认的 prefers-color-scheme。需要 dark: 变体时,在 Tailwind 入口 CSS 中声明:
@custom-variant dark (&:where([theme-mode='dark'], [theme-mode='dark'] *));
该变体只认 [theme-mode='dark'],不包含 .semi-always-dark。
3. 已知差异
层顺序正确时,Tailwind 的 Preflight 仍会改写 Semi 没有显式声明、依赖浏览器默认值(UA 默认值)的属性。以下组件在 Tailwind 页面中的表现与空白页不同:
| 组件/元素 | 属性 | 仅 Semi | 与 Tailwind 共用 |
|---|---|---|---|
MarkdownRender 的 hr | color、border-color | 灰色 | 黑色 |
MarkdownRender 外层 pre | font-family、font-size | monospace、13px | Tailwind 的等宽字体栈、16px |
Empty 中使用方传入的 <img width height> | height、max-width | 按 HTML 属性 | height: auto 与 max-width: 100% |
Semi 在根元素(:root)上显式声明了 line-height(20px 对 14px 的比例),组件高度不再随宿主页面的行高变化;只有 .semi-always-light、.semi-always-dark 子树继承页面自己的值。文档站使用 modern-normalize,空白页面与文档站的行高、元素默认样式不同,对照组件尺寸时请以同样的页面基座为准。
运行时覆盖 Token
Semi 的 Token 声明在 :root 上,随模式变化或引用调色板的 Token 还会在 [theme-mode='dark']、.semi-always-light、.semi-always-dark 上重新声明。覆盖这类 Token 时按模式各写一条未分层的规则,未分层的样式总是优先于 semi 层:
:root {
--semi-color-primary: rgb(0, 120, 90);
}
[theme-mode='dark'] {
--semi-color-primary: rgb(90, 200, 160);
}
只想改局部区域的模式时,还需要覆盖 .semi-always-light 或 .semi-always-dark(自定义 prefixCls 时为 .{prefixCls}-always-*)。
间距、字号、行高、字重、字体族、z-index、描边宽度、圆角、蒙层色以及 --semi-white、--semi-black 在两种模式下取值相同,只声明在 :root 上,一条 :root 规则即可覆盖,.semi-always-* 子树也会继承。其中 --semi-spacing-*(none、loose、extra-loose 除外)、--semi-height-control-*、--semi-border-thickness、--semi-border-thickness-control、--semi-z-table-fixed 在组件样式里参与了编译期计算,组件使用编译好的数值而不读取这些变量,覆盖它们只影响 Tailwind 原子类;要改组件需通过主题的 Sass 变量。