普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月2日首页

理解 CSS backface-visibility:卡片翻转效果背后的属性

作者 parade岁月
2025年12月2日 13:16

前段时间在做一个产品展示页,需要实现卡片翻转效果。本以为很简单,结果翻转的时候总是"穿帮"——正面和背面同时显示,或者背面的文字是镜像的。折腾了半天才发现,原来是 backface-visibility 这个属性没搞明白。

今天就来聊聊这个 CSS 属性。

🎮 在线演示frontend-learning.pages.dev/animation/d…

建议边看文章边打开演示页面,所有效果都可以直接交互体验。

先说结论

backface-visibility 控制的是:当一个元素的背面朝向你时,这个背面是否可见

听起来有点绕?没关系,我们慢慢来。

理解"背面"这个概念

这是理解整个属性的关键。很多人(包括我)一开始都没搞清楚这个"背面"到底是什么。

关键理解:每个 HTML 元素就像一张纸,天生就有正面和背面。当你用 rotateY(180deg) 翻转它时,你看到的是它的背面(就像纸翻过来,文字是镜像的)。

来看个最简单的例子:

<div
  style="
  background: linear-gradient(135deg, #10b981, #059669);
  color: white;
  padding: 40px;
  transform: rotateY(180deg);
"
>
  HELLO
</div>

你会看到什么?镜像的 HELLO。这就是元素的背面。

backface-visibility: hidden 的作用就是:当背面朝向你时,让它变透明

四个场景,逐步理解

我做了四个对比演示,帮你理解这个属性是怎么工作的。

场景 1:正常叠加

<div style="position: relative;">
  <div class="front" style="position: absolute;">FRONT</div>
  <div class="back" style="position: absolute;">BACK</div>
</div>

结果:只看到 BACK 原因:两个都是 absolute,BACK 在上层覆盖了 FRONT

这个很好理解,就是普通的 DOM 层叠。

场景 2:BACK 翻转 180°

.back {
  transform: rotateY(180deg);
}

结果:看到镜像的 BACK 原因:BACK 翻转后,你看到的是它的背面(文字镜像)

这里就出现问题了!虽然 BACK 翻转了,但你看到的是它的背面,文字是反的。

场景 3:隐藏 BACK 的背面

.back {
  transform: rotateY(180deg);
  backface-visibility: hidden; /* 关键!*/
}

结果:✅ 只看到 FRONT 原因:BACK 的背面被隐藏,露出下面的 FRONT

现在好了!BACK 的背面不可见了,所以你能看到下面的 FRONT。

场景 4:完美翻转

.front,
.back {
  backface-visibility: hidden; /* 两面都隐藏背面 */
}
.back {
  transform: rotateY(180deg);
}

/* 悬停时翻转父容器 */
.card-container:hover .card {
  transform: rotateY(180deg);
}

结果:✅ 悬停完美翻转 原理

  • 初始状态:FRONT 正面朝向你(显示),BACK 背面朝向你(隐藏)
  • 翻转后:FRONT 背面朝向你(隐藏),BACK 正面朝向你(显示)

这就是完美的卡片翻转效果!

语法很简单

.element {
  backface-visibility: visible | hidden;
}
  • visible:默认值,背面可见
  • hidden:背面不可见

就这两个值,没别的了。

一个常见误区:为什么不能分别旋转子元素?

这是我当时最困惑的地方。既然翻转后正面和背面都旋转了 180°,为什么不能直接这样写?

/* ❌ 错误写法 */
.card-container:hover .front {
  transform: rotateY(180deg);
}
.card-container:hover .back {
  transform: rotateY(180deg);
}

看起来很合理对吧?但实际上完全不行

问题在哪?

关键在于:CSS 的 transform 属性会被完全覆盖,而不是累加

/* 初始状态 */
.front {
  transform: rotateY(0deg);
}
.back {
  transform: rotateY(180deg);
}

/* 悬停后 */
.card-container:hover .front {
  transform: rotateY(180deg); /* ✓ 从 0° 变成 180° */
}
.card-container:hover .back {
  transform: rotateY(180deg); /* ✗ 从 180° 变成 180°,没变化! */
}

背面的初始值 rotateY(180deg) 被新值 rotateY(180deg) 覆盖,但因为值相同,所以背面根本没动!

正确做法:旋转父容器

/* ✅ 正确写法 */
.card-container:hover .card {
  transform: rotateY(180deg); /* 旋转整个父容器 */
}

这样做的原理是:子元素的 transform 是相对于父元素的坐标系

初始状态:
.card (0°)
  ├── .front: rotateY(0°) 相对于 .card   → 绝对位置 0°
  └── .back:  rotateY(180°) 相对于 .card → 绝对位置 180°

悬停后:
.card (180°)  ← 整个坐标系旋转了
  ├── .front: rotateY(0°) 相对于 .card   → 绝对位置 0° + 180° = 180°
  └── .back:  rotateY(180°) 相对于 .card → 绝对位置 180° + 180° = 360° = 0°

父元素旋转时,子元素的相对角度不变,但绝对角度会改变。这才是正确的翻转逻辑。

为什么需要三层结构?

标准的卡片翻转需要三层 DOM 结构:

<div class="爷容器">
  <!-- perspective(观察点) -->
  <div class="父容器">
    <!-- transform-style + rotateY(翻转者) -->
    <div class="正面"></div>
    <!-- backface-visibility(被翻转的面) -->
    <div class="背面"></div>
    <!-- backface-visibility(被翻转的面) -->
  </div>
</div>

每一层的职责

爷容器:设置 perspective

.card-container {
  perspective: 1000px; /* 必须在父元素上 */
}
  • 定义观察者的位置,创建 3D 空间
  • 类比:你站在舞台前看表演,这个属性决定你站在哪里看
  • 为什么在外层perspective 必须设置在父元素上,才能对子元素的 3D 变换生效

父容器:设置 transform-style 和执行 rotateY

.card {
  transform-style: preserve-3d; /* 保持 3D 空间 */
  transition: transform 0.6s;
}

.card-container:hover .card {
  transform: rotateY(180deg); /* 翻转整个卡片 */
}
  • preserve-3d:让子元素保持在 3D 空间中(而不是被压平)
  • 类比:这是舞台上的转盘,带着上面的演员一起旋转

子元素:正面和背面

.card-front,
.card-back {
  position: absolute;
  backface-visibility: hidden; /* 关键!隐藏背面 */
}

.card-back {
  transform: rotateY(180deg); /* 背面预先翻转 */
}
  • position: absolute:让两个面重叠在同一位置
  • 背面预先翻转 180°:这样当父容器翻转 180° 时,背面刚好正面朝向你

为什么不能少一层?

如果只有两层:

/* ❌ 错误 */
.card {
  perspective: 1000px;
  transform: rotateY(180deg);
}

问题perspectivetransform 在同一个元素上,perspective 不会对自己的 transform 生效,只会对子元素生效。结果就是没有透视效果。

完整的卡片翻转模板

直接复制这个模板,改改样式就能用:

<!-- HTML 结构 -->
<div class="card-container">
  <!-- 爷容器:观察点 -->
  <div class="card">
    <!-- 父容器:翻转者 -->
    <div class="card-front">正面内容</div>
    <div class="card-back">背面内容</div>
  </div>
</div>
/* 第 1 层:爷容器 - 设置观察点 */
.card-container {
  perspective: 1000px; /* 必须在父元素上 */
}

/* 第 2 层:父容器 - 执行翻转 */
.card {
  transform-style: preserve-3d; /* 保持 3D 空间 */
  transition: transform 0.6s;
}

.card-container:hover .card {
  transform: rotateY(180deg); /* 翻转整个卡片 */
}

/* 第 3 层:子元素 - 正面和背面 */
.card-front,
.card-back {
  position: absolute;
  width: 100%;
  height: 100%;
  backface-visibility: hidden; /* 关键!隐藏背面 */
}

.card-back {
  transform: rotateY(180deg); /* 背面预先翻转 */
}

这个模板是标准写法,三层结构缺一不可。

几个注意事项

  1. 必须配合 3D 变换backface-visibility 只在 3D 变换(如 rotateY, rotateX)中有意义,2D 旋转(rotate)不会产生背面
  2. 性能优化:设置为 hidden 可以让浏览器跳过背面的渲染,提升性能
  3. 浏览器兼容:现代浏览器都支持,旧版本可能需要 -webkit- 前缀

总结

backface-visibility 这个属性本身很简单,就两个值。但要真正理解它,需要搞清楚几个概念:

  1. 每个元素天生就有正面和背面
  2. backface-visibility: hidden 让背面朝向你时变透明
  3. 卡片翻转必须在父元素上执行旋转
  4. 标准的三层结构不能省略

最后再推荐一次我做的演示页面,里面有所有场景的交互演示和详细说明:

👉 frontend-learning.pages.dev/animation/d…

昨天 — 2025年12月1日首页

antd 4.x Tabs 点击阻止冒泡

2025年12月1日 18:20

一、场景

image.png

如上图所示,tab1-未回复,tab2-已回复+筛选条件仅看未处理
当在tab1时,点击tab2的仅看未处理checkbox,此时需要进行tab2的数据请求(请求已回复&未处理的数据)

二、基础实现

const [activeKey, setActiveKey] = useState('replied');

const defaultPageParams = {
    page: 1,
    rows: 5,
};

const getRepliedData = (params: any) => {
    const _params = {
        ...params
    }
    if (_params.handleStatus == null) {
        delete _params.handleStatus;
    }
    // 存一份params
    //...
}

const getNotReplyData = (params: any) => {
    //...
    // 存一份params
}

const handleFilterRepliedData = (e: CheckboxChangeEvent) => {
    const params: any = {
        ...defaultPageParams,
        handleStatus: e.target.checked ? 0 : null,
    };
    getRepliedData(params);
}

const getData = (key: string) => {
    const params: any = {
        ...defaultPageParams
    };
    if (key === 'replied') {
      getRepliedData(params);
    }
    if (key === 'not-reply') {
      getNotReplyData(params);
    }
};
<Tabs
    defaultActiveKey={'not-reply'}
    activeKey={activeKey}
    onChange={(key) => {
        setActiveKey(key);
        getData(key);
    }}
    items={[
        {
          key: 'not-reply',
          label: '未回复',
          children: (
            <div>未回复内容</div>
          ),
        },
        {
          key: 'replied',
          label: (
            <Space>
                <div>已回复</div>
                <Checkbox onChange={handleFilterRepliedData}>
                  僅看未處理
                </Checkbox>
            </Space>
          ),
          children: (
            <div>已回复内容</div>
          ),
        },
    ]}
/>

三、基础实现存在的问题

在tab1直接点击tab2的checkbox,会执行Checkbox的onChange事件,也会执行Tabs的onChange事件,会导致请求了两次接口同时页面上会有数据闪现现象,若Tabs的请求更慢,可能还会导致数据查询异常。

四、优化实现

关键代码:

  1. Checkbox包一层:
    <span style={{ pointerEvents: 'none' }} onClick={(e) => e.stopPropagation()}>
  2. Checkbox加style:
    style={{ pointerEvents: 'auto' }}
  3. Checkbox onChange方法添加代码:
    e.stopPropagation();
    setActiveKey('replied');
// 1、改Checkbox的onChange方法
const handleFilterRepliedData = (e: CheckboxChangeEvent) => {
    // 避免在未命中該tab的情況下,直接點擊該checkbox請求了兩次接口導致的頁面內容閃現問題
    e.stopPropagation();
    setActiveKey('replied');
    
    //...
}

<Tabs
    //...
    items={[
        //...
        {
          key: 'replied',
          label: (
            <Space>
                <div>已回复</div>
                {/* 2、改Checkbox视图,解決事件冒泡到tabs的onChange事件 */}
                <span style={{ pointerEvents: 'none' }} onClick={(e) => e.stopPropagation()}>
                    <Checkbox style={{ pointerEvents: 'auto' }} onChange={handleFilterRepliedData}>
                      僅看未處理
                    </Checkbox>
                </span>
            </Space>
          ),
          children: (
            <div>已回复内容</div>
          ),
        },
    ]}
/>

从border-image 到 mask + filer 实现圆角渐变边框

2025年12月1日 17:12

用 CSS Mask + Filter 实现高级渐变圆角边框

前言

故事开始于一张恶心人的设计UI稿开始,由于签了保密协议,只能切割设计稿,展示恶心的片段; 最近手头刚好有个大屏的项目,我们的设计师,于是乎,搞出了如下片段:

7784e481-facf-4d3b-a285-0a13b18f9101.png

90be2647-c1c8-450c-95c7-8684b83ecd8b.png

10c91969-8c93-4028-9036-f5a66479ec0a.png

650c9eaf-a76d-4502-a144-65d78a52aab2.png

cdc2af44-ae1d-43a9-90be-0f0c5001a10b.png

各位jym,你们想到哪些方案呢?评论区见! 最简单省事粗暴的方案,就是UI直接给切图,但俺们是有追求的(其实以前也干过),性能要有要求的于是乎采用以下方案实现!

前面2张图很好实现,border-image 渐变既可以很好实现; 后面三张设计图,是有圆角的,border-image 无法实现圆角,border-radius 可以实现圆角但无法实现渐变边框;

故事主角出现了mask + filter

图一:border-image:linear-gradient(90deg, #038AFE 0%, rgba(3, 138, 254, 0.3) 48%, #038AFE 100%) 0.5

图二:border-image: linear-gradient(90deg, #12c1ea 0%, rgba(3, 138, 254, .3) 50%, #12c1ea 100%) 1 1;

图三:

.mask{
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    border: 1px solid transparent;
    border-radius: 10px;
    -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
    mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;
    z-index: 0;
}

.mask::after{
    content: '';
    position: absolute;
    bottom: -5px;
    left: 0;
    right: 0;
    height: 100%;
    background: linear-gradient(180degrgba(01742550.250%#00AEFF 100%);
    filter: blur(10px);
    z-index: 0;
}

图四:跟图三其实一样的,只是伪类的高度设置不一样

.mask::after{
    content: '';
    position: absolute;
    bottom: -5px;
    left: 0;
    right: 0;
    height: 80%;
    background: linear-gradient(180degrgba(01742550.250%#00AEFF 100%);
    filter: blur(10px);
    z-index: 0;
}

图五:遮罩层都一样,不一样的是伪类,渐变色设置的技巧

.mask::after{
    content: '';
    position: absolute;
    bottom: -5px;
    left: 0;
    right: 0;
    height: 80%;
    background: linear-gradient(180deg#00AEFF 0%rgba(1179255019%rgba(1179255077%#00AEFF 96%);
    filter: blur(10px);
    z-index: 0;
}

核心概念

CSS Mask 属性

mask 属性允许我们使用图像、SVG 或渐变作为遮罩,控制元素的可见区域。它的工作原理类似于 Photoshop 中的遮罩层。

基本语法:

mask: <mask-source> <mask-mode> <mask-position> / <mask-size> <mask-repeat> <mask-origin> <mask-clip> <mask-composite>;

其中,mask-composite 属性定义了多个遮罩层如何组合。对于实现渐变边框,我们主要使用 exclude 值,它会显示两个遮罩层的非重叠区域。

daa70877-b139-4c1c-a697-43f66c387025.png

CSS Filter 属性

filter 属性用于对元素应用图形效果,如模糊、对比度、亮度等。我们可以结合 filter 来增强渐变边框的视觉效果。

常用 filter 函数:

函数名 描述 示例
blur() 模糊效果 blur(10px)
contrast() 对比度调整 contrast(150%)
brightness() 亮度调整 brightness(120%)
saturate() 饱和度调整 saturate(200%)
opacity() 透明度调整 opacity(0.8)

实现方法

方法一:基础 Mask 渐变边框

核心思路: 使用两层渐变遮罩,通过 mask-composite: exclude 实现边框效果。

7f260658-5a1a-4d1e-967b-93aba5445ceb.png

<div class="gradient-border basic">
    <h3>基础渐变边框</h3>
    <p>使用 mask-composite: exclude 实现</p>
</div>
.gradient-border.basic {
    /* 背景渐变 */
    background: linear-gradient(45deg, #96ceb4, #ffeead, #ff6b6b);
    
    /* 遮罩 */
    mask: 
        /* 内层遮罩:白色矩形,大小与元素相同,有圆角 */
        linear-gradient(#fff 0 0) content-box,
        /* 外层遮罩:白色矩形,大小与元素相同 */
        linear-gradient(#fff 0 0);
    /* 设置遮罩属性 */
    mask-composite: exclude;
    /* 内边距,控制边框宽度 */
    padding: 6px;
}

效果说明: 内层遮罩显示元素内容区域,外层遮罩显示整个元素,通过 exclude 组合,只显示两层遮罩的非重叠区域,即边框部分。

方法二:伪元素 + Mask

核心思路: 使用伪元素创建渐变背景,通过 mask 属性控制显示区域。

dfcf36b1-eb00-46f0-a482-472505e5dc0a.png

<div class='demo'>
    <div class="gradient-border pseudo-element">
        <h3>伪元素 + Mask</h3>
        <p>使用 ::before 伪元素创建渐变背景</p>
    </div>
</div>
.demo {
    position:relative;
}
.gradient-border.pseudo-element {
    position: absolute; 
    top: 0; 
    right: 0; 
    bottom: 0; 
    left: 0; 
    border: 1px solid transparent; 
    border-radius: 10px; -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0); 
    mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude; z-index: 0;
}

.gradient-border.pseudo-element::before {
    content: ''; 
    position: absolute; 
    bottom: -1px; 
    left: -1px; 
    right: -1px; 
    top:-1px;
    background: linear-gradient(180degrgba(01742550.250%#00AEFF 100%); filter: blur(10px);
    z-index: 0;
}

效果对比

实现方式 特点 适用场景
基础 Mask 简洁、性能好 简单渐变边框需求
伪元素 + Mask 灵活、易于控制 需要复杂边框样式

浏览器兼容性

CSS Mask 属性的浏览器支持情况如下:

浏览器 版本
Chrome 85+
Firefox 70+
Safari 15+
Edge 85+

对于不支持 mask-composite: exclude 的浏览器,我们可以使用 -webkit-mask-composite: xor 作为替代:

.gradient-border {
    mask-composite: exclude;
    /* Safari 兼容性 */
    -webkit-mask-composite: xor;
}

最佳实践

  1. 选择合适的实现方式:根据需求和浏览器支持情况,选择最适合的实现方式。
  2. 性能优化:避免在大量元素上同时使用复杂的 mask 和 filter 效果,这可能会影响页面性能。
  3. 降级方案:为不支持 mask 属性的浏览器提供降级样式,例如使用传统的 border 或伪元素方法。
  4. 渐变颜色选择:选择对比度适中、和谐的渐变颜色,避免过于刺眼的颜色组合。
  5. 边框宽度:边框宽度不宜过宽,一般建议在 2-8px 之间,这样视觉效果最佳。

总结

使用 CSS 的 maskfilter 属性实现渐变圆角边框是一种高级且灵活的方法,它具有以下优点:

  1. 代码简洁:相比传统的嵌套元素或复杂伪元素方法,代码更加简洁和易于维护。
  2. 效果丰富:可以实现多种高级效果,如毛玻璃边框、动态渐变边框等。
  3. 灵活可控:可以通过调整 mask 属性和 filter 属性,精确控制边框的外观和效果。
  4. 性能优良:相比 JavaScript 实现的动态边框,CSS 实现的性能更好。

虽然 mask 属性的浏览器支持还不是 100%,但在现代浏览器中已经得到了很好的支持。通过提供适当的降级方案,我们可以在项目中安全地使用这种方法。

希望本文对你理解和使用 CSS Mask + Filter 实现渐变圆角边框有所帮助!如果你有任何问题或想法,欢迎在评论区留言讨论。

参考资料

写Tailwind CSS像在写屎山?这锅该不该它背

2025年12月1日 15:52

我上次在群里吐槽Tailwind,被几个大佬围攻了:“现在还在写传统CSS的怕不是还在用jQuery?”、“都2025年了还用BEM?”,整得我都不敢说话了。

作为一个前端搬砖工,我从Nodejs到React再到Vue都踩过一遍坑,今天就跟大伙儿聊聊这个让我又爱又恨的Tailwind。

一、为什么我觉得Tailwind有时候真的很操蛋

1. 这HTML还能看吗?

这是我第一次看到Tailwind代码的反应:

<div class="flex flex-col md:flex-row items-center justify-between p-4 md:p-6 lg:p-8 bg-white dark:bg-gray-900 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300">
  <!-- 还有一堆嵌套div,每个都带着几十个类名 -->
</div>

同事问我:“这坨代码什么意思?”我看了半天说:“一个卡片,会动,能响应式,深色模式适配了……”但我心里想的是:这TM跟当年在HTML里写style="color: red; font-size: 14px;"有啥本质区别?

2. 接手别人的Tailwind项目有多痛苦

上个月接了个离职同事的项目,打开一看差点没背过气去:

<div className={`px-${size === 'large' ? 6 : size === 'small' ? 2 : 4} py-${hasIcon ? 3 : 2} ${variant === 'primary' ? 'bg-blue-500' : 'bg-gray-200'} ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'hover:opacity-90'}`}>
  {/* 还有50行类似的代码 */}
</div>

这种动态拼接类名的操作,让我调试的时候想砸键盘。查了半天发现有个按钮在某种状态下padding不对,原来是px-${size}这种骚操作导致的。

3. 这玩意真的能提高开发效率吗?

老板跟我说:“用Tailwind开发速度快啊!”但真实情况是:

  • 边写边查文档:m-4p-4到底哪个是margin哪个是padding?mt-4mr-4又是啥?
  • 遇到复杂布局:用flex还是grid?Tailwind的grid类名又长又难记
  • 调个细节样式:想微调一个阴影,得查半天文档才知道shadow-lgshadow-xl的区别

有这查文档的时间,我CSS早写完了。

二、但为什么大佬们都在吹爆Tailwind?

1. 等我真的用起来之后……

两个月后,当我对常用类名烂熟于心后,发现有些场景真香:

快速原型开发:产品经理站我身后:“这里改个间距,那里调个颜色,这个按钮hover效果换一下……”

以前:切到CSS文件 -> 找到对应的类 -> 修改 -> 切回来预览 -> 重复 现在:直接在HTML里改几个类名 -> 实时预览

设计一致性:以前团队里每个开发者对“大间距”的理解都不一样,有人写margin: 20px,有人写margin: 24px,还有人写margin: 1.5rem。现在统一用m-5m-6,UI终于统一了。

2. 性能确实牛逼

我原来不信,直到对比了项目打包后的CSS文件大小:

  • 之前的项目(手写CSS):main.css 87KB
  • 现在的项目(Tailwind + JIT):main.css 12KB

因为Tailwind只生成你用到的样式,不会有未使用的CSS代码。

3. 再也不用想类名了

还记得那些年被BEM命名支配的恐惧吗?

.card {}
.card__header {}
.card__header--active {}
.card__body {}
.card__footer {}
.card__footer__button {}
.card__footer__button--disabled {}

现在?直接写样式就行了,不用再想header-wrapper-inner-content这种傻逼名字了。

三、我从抗拒到真香的转变

转折点是我开始用正确的方式写Tailwind

错误示范 ❌

// 直接把所有类名堆在组件里
function BadButton() {
  return (
    <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
      提交
    </button>
  );
}

正确姿势 ✅

// 1. 先封装基础组件
function Button({ 
  children, 
  variant = 'primary',
  size = 'medium',
  fullWidth = false 
}) {
  const baseClasses = "font-bold rounded transition-colors";
  
  const variants = {
    primary: "bg-blue-500 hover:bg-blue-700 text-white",
    secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
    danger: "bg-red-500 hover:bg-red-700 text-white"
  };
  
  const sizes = {
    small: "py-1 px-3 text-sm",
    medium: "py-2 px-4",
    large: "py-3 px-6 text-lg"
  };
  
  const widthClass = fullWidth ? "w-full" : "";
  
  return (
    <button className={`${baseClasses} ${variants[variant]} ${sizes[size]} ${widthClass}`}>
      {children}
    </button>
  );
}

// 2. 使用 cva 库管理变体(更优雅)
import { cva } from 'class-variance-authority';

const buttonVariants = cva(
  "font-bold rounded transition-colors", // 基础样式
  {
    variants: {
      variant: {
        primary: "bg-blue-500 hover:bg-blue-700 text-white",
        secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
      },
      size: {
        small: "py-1 px-3 text-sm",
        medium: "py-2 px-4",
      }
    },
    defaultVariants: {
      variant: "primary",
      size: "medium"
    }
  }
);

// 3. 实际使用
function GoodButton() {
  return (
    <Button variant="primary" size="large">
      提交
    </Button>
  );
}

四、什么时候该用,什么时候不该用

赶紧用起来吧 👍

  1. 新项目,尤其是React/Vue/Svelte项目:组件化能很好解决Tailwind的可维护性问题
  2. 需要统一设计规范:设计系统配好了,大家就按这个来,别TM再自己发挥了
  3. 内部管理系统、后台项目:快速迭代,老板天天改需求,这种场景Tailwind无敌
  4. 团队协作项目:不用再解释为什么这里用margin-top: 8px而不是10px

算了,别用了 ❌

  1. 静态小网站:就几个页面,写点CSS完事了,别折腾
  2. 老项目迁移:除非你想加班加到死
  3. 完全不懂CSS的新手:Tailwind不是CSS的替代品,它是工具。连CSS盒模型都不懂就用Tailwind,等于不会开车就用自动驾驶
  4. 设计师天马行空:如果你们设计师每个页面风格都不一样,用Tailwind配置会把你逼疯

五、我总结的血泪经验

  1. 不要直接在JSX里堆类名:这是所有屎山的源头!一定一定要封装成组件
  2. 配置好自己的设计系统:别用默认配置,根据项目需求配一套自己的tailwind.config.js
  3. 善用 @apply:重复出现的样式组合,用@apply提取
/* 在CSS文件中 */
.btn-primary {
  @apply bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded;
}
  1. 结合现代工具链clsx处理条件类名,tailwind-merge解决类名冲突
  2. 定期重构:发现重复的样式组合就抽象,别懒!

最后说句实话

用不用Tailwind,其实跟你用什么技术关系不大,关键看你怎么用。

那些说Tailwind垃圾的,多半是看到了滥用它的项目;那些吹爆Tailwind的,多半是用对了方法。

就像当年大家吵jQuery和原生JS,吵React和Vue一样,最后你会发现:工具没有对错,只有适不适合。 牛逼的程序员用记事本都能写出好代码,菜鸡用再牛逼的框架也能写出屎山。

所以,别吵了,赶紧去写代码吧。老板又改需求了,今天还得加班呢。


关注公众号" 大前端历险记",掌握更多前端开发干货姿势!

❌
❌