tailwindcss 如何覆盖组件库的默认样式

711 阅读3分钟

tailwindcss现在用的人越来越多了,深度使用后才知道有多香。但是有个烦恼困扰过我很久,不知道小伙伴们有没有遇到过,那就是组件库里的一些样式,如果不支持传入class去修改的话,那么会如果覆盖组件库的基础样式将是一个巨大的困扰。

当然最简单的方法就是再加个css文件,用css的方式去覆盖组件库的样式。

举个例子:

假设我基于 antd 的 card 封装了这么一个 React 组件

import { Card } from 'antd';
import classNames from 'classnames';
import React, { HTMLProps } from 'react';

export interface MyCardProps extends HTMLProps<HTMLDivElement> {}

export const MyCard = (props: MyCardProps) => {
  const { className = '', ...restProps } = props;
  return (
    <div className={classNames('flex items-center justify-center', className)} {...restProps}>
      <Card title="测试样式覆盖" extra={<a href="#">More</a>} className="w-[500px]" hoverable>
        <p>Card content</p>
        <p>Card content</p>
        <p>Card content</p>
        <div className="text-orange-300">特殊的 line</div>
      </Card>
    </div>
  );
};

可以看到这个组件封装的很糟糕,只暴露了最外层的className给外部使用。

此时组件渲染出来长这样:

好的现在我们要在调用MyCard组件的地方把卡片的 title 改成红色。

使用css进行覆盖:

这个时候常见的做法是引入一个css文件,直接

import './index.css'

<MyCard className={classNames('my-card')} />

因为antd中title的class是 ant-card-head-title

所以我们在css文件中可以写下:

.my-card .ant-card-head-title {
    color: red;
}

这样就顺利完成了样式覆盖。这样太麻烦了,我都用了tailwindcss你还要我用css,那又要考虑css起名这个千古难题,还有命名冲突或者用 css module 来进行样式隔离。

那有没有更好的办法呢?

使用 tailwindcss 进行组件库样式的覆盖

matchVariant 函数

tailwindcss 官方是提供了解决办法的,答案就在 tailwindcss 的插件里的 matchVariant 函数里

关于matchVariant的官方示例可以参考:Plugins - Tailwind CSS

可以看到matchVariant接受三个参数:

  • 指令名称——字符串
  • 指令的转换函数——函数
  • 配置对象(可选)

简单示例

于是我们在 tailwind.config.js 的plugin中添加一个plugin:

plugins: [
  plugin(function ({ addUtilities, addComponents, e, config, matchVariant }) {
    matchVariant('rp', (value) => {
      console.log('replace value ====', value); // 你可以打印一下看看编译的时候,收到的是什么值
      return `& ${value)}`;
    });
  })
]

函数很简单,我们的自定义指定是 rp ,就是replace的简写,然后返回 `& ${value)}`

这时候我们去写代码的时候就会发现有提示了(前提是你安装了tailwindcss的插件),如果没有 reload 一下你的vsCode。

要把 MyCard 组件的标题改成红色,这么写就好了

<MyCard className={classNames('rp-[.ant-card-head-title]:text-red-500')} />

然后看看效果。

为什么可以生效

我们把鼠标hover到刚才写的class上看看编译出来的是个什么东西?

就是下面这个东西:

#root :is(.rp-[.ant-card-head-title]:text-red-500 .ant-card-head-title) {
  --tw-text-opacity: 1;
  color: rgb(239 68 68 / var(--tw-text-opacity));
}

#root 是我在 tailwind.config.js 里加的import,来提升权重,

:is(.rp-[.ant-card-head-title]:text-red-500 .ant-card-head-title) is是伪类选择函数,其实这里用不用is是一样的,不用纠结这个。可以看到这里就是和我们上面自己用css写样式覆盖的原理是一样的。

ps:is的精髓是里面的元素用逗号分隔,形成一个或的关系,参考::is() - CSS:层叠样式表 | MDN (mozilla.org)

这样我们就简单的实现了针对某个样式的样式覆盖。

但是问题又来了,我想把最后一行的div加个背景色,该怎么办?

如果我们直接用刚才的方式写下 rp-[.ant-card-body div]:bg-orange-200 是不生效的。

因为这个空格在 tailwindcss 里是解析不了的,但是我们最后编译出来的需要有个空格的,所以我们可以把刚才的plugin改一下,使用下划线当空格。

plugins: [
  plugin(function ({ addUtilities, addComponents, e, config, matchVariant }) {
    matchVariant('rp', (value) => {
      console.log('replace value ====', value); // 你可以打印一下看看编译的时候,收到的是什么值
      return `& ${value.replace(/_/g, ' ')}`;
    });
  })
]

这样就可以正确生效了。

and more:

插件还有很多强大的功能,比如 addUtilities加个自定义的class类名。

  plugins: [
    plugin(function ({ addUtilities, addComponents, e, config, matchVariant }) {
      // Add your custom styles here
      console.log('my plugin ====');
      addUtilities({
        '.inline-box-border-gray': {
          'box-shadow': '0 0 0 2px inset gray'
        }
      });
      matchVariant('rp', (value) => {
        console.log('replace value ====', value);
        return `& ${value.replace(/_/g, ' ')}`;
      });
    })
  ]

上面的 addUtilities 我增加了一个自定了class inline-box-border-gray