一.为什么要使用tailwind-merge?

主要是为了解决tailwindcss中样式冲突时不能很好的按照我们的css层叠想法去合并冲突类。

官方文档:https://tailwindcss.com/docs/styling-with-utility-classes#managing-style-conflicts

有一个例子是:

<div class="grid flex"> <!-- ... --></div>

请问上面的样式最后是应用哪个display的属性? 按照我们的经验,肯定觉得是flex,但是实际上是grid,原因是因为taiwindcss时根据样式表的顺序来应用的,而不是我们自己写的顺序

当你使用同样的一个样式的时候,跟我们以往的经验不同,在后面的样式不会重叠前面的。

另外一个例子就是

<div className="px-2 py-1 p-3 w-10 h-10 bg-amber-200"></div>

这样子,你觉得最终是多少呢?

✅ 最终结果:
由于 px-* 和 py-* 类在样式表中定义得比 p-* 类更晚,所以:
最终的 padding 效果是:
padding-top: 0.25rem; (4px) - 来自 py-1
padding-bottom: 0.25rem; (4px) - 来自 py-1
padding-left: 0.5rem; (8px) - 来自 px-2
padding-right: 0.5rem; (8px) - 来自 px-2
📝 为什么不是 p-312px 全方向?
因为在 Tailwind 的样式表中,方向性的 padding 类(px-*, py-*)定义在通用 padding 类(p-*)之后,所以它们会覆盖 p-3 的效果。
简单说:最终是上下 4px,左右 8px 的 padding! 

相信大家肯定觉得很难受了,所以就引出了下一个工具:tailwind-merge

这个工具就能按照我们的想法来合并css,后面我也会写一个关于tailwind-merge的使用。

二.基本使用

1.安装

pnpm i tailwind-merge

2.使用:

import { twMerge } from 'tailwind-merge'

export default function TailwindMerge() {
    // 在组件内部使用
    const className = twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]')
    console.log(className) // 每次组件渲染时执行
    合并后的类名:hover:bg-dark-red p-3 bg-[#B91C1C]
    
    return (
        <div className={className}>
            <h1>Tailwind Merge</h1>
            <p>合并后的类名:{className}</p>
        </div>
    )
}

可以看到上面的使用还是很方便的

3.坑点

文档:https://github.com/dcastil/tailwind-merge/blob/v3.4.0/docs/configuration.md#usage-with-custom-tailwind-config
虽然这样子使用很方便,但是它有个坑点,就是之前我们也提到过,我们自己在tailwind里面会去自定义自己的样式,由于tailwind-merge不认识我们自定义的样式,然后会导致它在合并的时候会被保留。比如:

/* 
  1. @utility - 定义原子级工具类(Tailwind v4 新特性)
  - 用途:创建可复用的单一用途工具类
  - 特点:会被 Tailwind 的 JIT 引擎处理,支持响应式和伪类变体
  - 可以使用:md:flex-center、hover:flex-center 等
*/
@utility flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

@utility flex-center-x {
  display: flex;
  justify-content: center;
}

@utility flex-center-y {
  display: flex;
  align-items: center;
}
<div className={twMerge("grid flex-center")}>aaa</div>
f12中css选择器该div上还有两个类名"grid flex-center" ,最后是'grid'布局,跟预期不符

4.如何解决自定义类的问题?

使用tailwind-merge自定义配置来告诉自己定义的类是属于哪一类的,然后进行合并。代码我放在下面了:

import { type ClassValue, clsx } from 'clsx';
import {
  createTailwindMerge,
  getDefaultConfig,
  mergeConfigs,
} from 'tailwind-merge';

const defaultConfig = getDefaultConfig();

/* ============================================================================
   配置说明:什么需要配置,什么不需要配置
   ============================================================================
   
   ✅ 需要配置的情况:
   
   1. @theme 中定义的非标准名称(如自定义的 font-size、shadow 等)
      例如:--font-size-menu-choice → 生成 text-menu-choice
      虽然会自动生成,但建议配置以确保 tailwind-merge 能识别
   
   2. @utility 定义的组合类(包含多个 CSS 属性)
      例如:@utility text-s-title-s { font-size + font-weight + line-height }
      必须配置!需要创建独立 classGroup + conflictingClassGroups
   
   3. @utility 定义的自定义工具类(如 flex-center)
      例如:@utility flex-center { display + align-items + justify-content }
      需要配置到对应的 classGroup 中
   
   ❌ 不需要配置的情况:
   
   1. @theme 中的标准颜色、间距(会自动生成标准 Tailwind 类)
      例如:--color-brand → 生成 text-brand, bg-brand, border-brand
      例如:--spacing-100 → 生成 p-100, m-100, w-100, h-100
      tailwind-merge 默认就能识别这些标准格式!
   
   2. @apply 定义的组件类(如 .avatar、.btn-primary)
      这些不是工具类,tailwind-merge 会把它们当作普通字符串处理
      不需要配置,也不建议在 tailwind-merge 中使用
   
   ============================================================================ */

// ===== 1. @theme 定义的自定义字体大小 =====
// 对应 CSS: --font-size-menu-choice, --font-size-title-l 等
// 生成的类: text-menu-choice, text-title-l 等
const customFontSizes = [
  'menu-choice',
  'menu-not-choice',
  'title-l',
  'title-m',
  'title-s',
  'number-l',
  'number-m',
  'number-s',
  'body-l',
  'body-m',
  'body-s',
  'tips',
];

// ===== 2. @utility 定义的文字样式组合类 =====
// 对应 CSS: @utility text-s-title-s { font-size + font-weight + line-height }
// 特点: 包含多个 CSS 属性,需要与多个 classGroup 冲突
// 必须配置: 创建独立组 + conflictingClassGroups
const textStyleUtilityClasses = [
  's-title-s',
  's-title-m',
  's-title-l',
  's-body-s',
  's-body-m',
  's-body-l',
];

export const twMergeConfig = mergeConfigs(defaultConfig, {
  extend: {
    classGroups: {
      /* -------------------------------------------------------------------------
         字体大小相关配置
         ------------------------------------------------------------------------- */
      
      // ✅ 需要配置:@theme 中定义的自定义字体大小
      // 对应 CSS: --font-size-menu-choice 等
      // 生成的类: text-menu-choice, text-title-l 等
      'font-size': [
        {
          text: customFontSizes,
        },
      ],
      
      /* -------------------------------------------------------------------------
         组合类配置(包含多个 CSS 属性的 @utility 类)
         ------------------------------------------------------------------------- */
      
      // ✅ 必须配置:@utility 定义的文字样式组合类
      // 对应 CSS: @utility text-s-title-s { font-size + font-weight + line-height }
      // 原因: 包含多个属性,需要与 font-size、font-weight、line-height 都冲突
      'text-style-combo': [
        {
          text: textStyleUtilityClasses,
        },
      ],
      
      /* -------------------------------------------------------------------------
         其他自定义工具类
         ------------------------------------------------------------------------- */
      
      // ✅ 需要配置:@theme 中定义的自定义阴影
      // 对应 CSS: --shadow-d-base, --shadow-d-dropdown 等
      // 生成的类: shadow-d-base, shadow-d-dropdown 等
      shadow: [
        {
          shadow: ['d-base', 'd-dropdown', 'd-button'],
        },
      ],
      
      // ✅ 需要配置:@utility 定义的布局工具类
      // 对应 CSS: @utility flex-center { display + align-items + justify-content }
      // 原因: 自定义类,需要加入 display 组才能与 flex、grid 等冲突
      display: [
        'flex-center',
        'flex-center-x',
        'flex-center-y',
      ],
      
      /* -------------------------------------------------------------------------
         ❌ 不需要配置的内容(tailwind-merge 默认能处理)
         ------------------------------------------------------------------------- */
      
      // ❌ 不需要配置:@theme 中的标准颜色
      // 例如: --color-brand-color → 自动生成 text-brand-color, bg-brand-color 等
      // tailwind-merge 默认能识别所有 text-*, bg-*, border-* 等标准格式
      
      // ❌ 不需要配置:@theme 中的标准间距
      // 例如: --spacing-100 → 自动生成 p-100, m-100, w-100, h-100 等
      // tailwind-merge 默认能识别所有 p-*, m-*, w-*, h-* 等标准格式
      
      // ❌ 不需要配置:@apply 定义的组件类
      // 例如: .avatar, .btn-primary, .card
      // 这些不是工具类,tailwind-merge 会当作普通字符串处理
      // 而且不建议在 tailwind-merge 中使用(见 TAILWIND_MERGE_BEST_PRACTICES.md)
    },
    
    /* ---------------------------------------------------------------------------
       冲突关系配置
       ---------------------------------------------------------------------------
       说明:只有包含多个 CSS 属性的组合类需要配置冲突关系
       例如:text-s-title-s 包含 font-size + font-weight + line-height
       必须声明它与这三个 classGroup 都冲突
       --------------------------------------------------------------------------- */
    conflictingClassGroups: {
      // text-style-combo 与三个样式类都冲突
      'text-style-combo': ['font-size', 'font-weight', 'line-height'],
      
      // 反向声明:让 tailwind-merge 知道双向冲突关系
      'font-size': ['text-style-combo'],
      'font-weight': ['text-style-combo'],
      'line-height': ['text-style-combo'],
    },
  },
});

export const twMerge = createTailwindMerge(() => twMergeConfig);

/**
 * 合并 Tailwind CSS 类名的工具函数
 * 
 * 功能:
 * - 自动合并冲突的 Tailwind 类(后面的覆盖前面的)
 * - 支持条件类名(使用 clsx)
 * - 支持自定义的 @utility 组合类
 * 
 * @example
 * // 基础合并:后面的覆盖前面的
 * cn('px-2 py-1', 'p-3') // => 'p-3'
 * 
 * @example
 * // 条件类名
 * cn('text-red-500', condition && 'text-blue-500')
 * // => 'text-blue-500' (if condition is true)
 * 
 * @example
 * // 组合类之间的合并
 * cn('text-s-title-s', 'text-s-title-l') // => 'text-s-title-l'
 * 
 * @example
 * // 组合类与单独属性类的合并
 * cn('text-2xl font-bold', 'text-s-title-s')
 * // => 'text-s-title-s' (删除了 text-2xl 和 font-bold)
 * 
 * @example
 * // 单独属性类覆盖组合类
 * cn('text-s-title-s', 'font-bold')
 * // => 'font-bold' (删除了 text-s-title-s)
 */
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

导入使用:

import { twMerge } from "tailwind-merge";
import { cn } from "@/utils/cn";
<div className={twMerge("grid flex-center")}>aaa</div>
<div className={cn("grid flex-center")}>bbb</div>

效果:

可以看到,因为配置了flex-center是属于display类别的,所以会和前面的grid进行合并。

5.注意点:

flex-center定义的类display: flex;align-items: center;justify-content: center;都是同一类别的,所以可以直接配置到tailwind-merge的display中。但是下面的text-s-title-s就是不同的类别。

/* 
  定义文字样式组合类 - 使用 @utility
  这样定义的类支持响应式变体,如:md:text-s-title-s
*/
@utility text-s-title-s {
  font-size: 16px;
  font-weight: 500;
  line-height: 16px;
}

@utility text-s-title-m {
  font-size: 20px;
  font-weight: 500;
  line-height: 20px;
}

@utility text-s-title-l {
  font-size: 24px;
  font-weight: 600;
  line-height: 24px;
}

所以我们在tailwind配置中是这样子配置的:

conflictingClassGroups: {
      // text-style-combo 与三个样式类都冲突
      'text-style-combo': ['font-size', 'font-weight', 'line-height'],
      
      // 反向声明:让 tailwind-merge 知道双向冲突关系
      'font-size': ['text-style-combo'],
      'font-weight': ['text-style-combo'],
      'line-height': ['text-style-combo'],
    },

为什么要这样子配置:

原因就是tailwind是原子级别的,所以tailwind-merge默认你定义的类名也是,只会去替换你在配置中定义的类别。如果我们只定义这个

'font-size': [
        {
          text: [...customFontSizes ,...textStyleUtilityClasses],
        },
      ],

那么text-s-title-s只会去覆盖前面的font-size,但是不会去覆盖font-weight和line-height

最好不要使用apply来自定义组合类(因为你不知道这个组合类应该放在配置项的哪一块。比如上面的flex-center是属于定位的,就是放在display这里),不然tailwind-merge识别不了,解释如文档。然后他会把识别不了的就不进行合并,应用到div上面去。

三.与clsx一起使用

1.clsx是什么?

动态类名拼接库。这个一般都是用classNames和clsx

a. 基本介绍

classNames: 最早的条件类名合并库(2015年),功能全面但体积稍大

clsx: 更轻量、更快的替代方案(2018年),与 classNames API 兼容,体积更小(~200B)

clsx 和 classNames 功能完全一致,clsx 更轻量更快

两者都只做条件拼接,不处理 Tailwind 类名冲突

tailwind-merge 专门解决 Tailwind 类名冲突问题

最佳组合: cn = twMerge + clsx(您的项目可以考虑这样做)

您当前的项目已经有 tailwind-merge,如果需要添加条件类名功能,建议安装 clsx 并创建 cn 工具函数!

2.为什么要使用动态类名拼接库?

// ❌ 传统方式 - 难以维护
function Button({ variant, size, disabled, loading, className }) {
  let btnClass = 'btn';
  if (variant === 'primary') btnClass += ' btn-primary';
  if (variant === 'secondary') btnClass += ' btn-secondary';
  if (size === 'sm') btnClass += ' btn-sm';
  if (size === 'lg') btnClass += ' btn-lg';
  if (disabled) btnClass += ' btn-disabled';
  if (loading) btnClass += ' btn-loading';
  if (className) btnClass += ' ' + className;
  
  return <button className={btnClass}>Click</button>;
}

// ✅ 使用 clsx - 清晰直观
function Button({ variant, size, disabled, loading, className }) {
  return (
    <button className={clsx(
      'btn',
      {
        'btn-primary': variant === 'primary',
        'btn-secondary': variant === 'secondary',
        'btn-sm': size === 'sm',
        'btn-lg': size === 'lg',
        'btn-disabled': disabled,
        'btn-loading': loading
      },
      className  // 外部传入的类名
    )}>
      Click
    </button>
  );
}

3.为什么要与clsx一起使用?

如见该文章

4.总结

clsx和classNames一样,主要是为了我们程序员好书写类名,但是不会去处理那些有冲突的样式, 但是tailwind-merge拿到clsx返回的类名,会把这些类名进行合并,然后给到div合并后的类名。

四.最终封装

import { type ClassValue, clsx } from 'clsx';
import {
  createTailwindMerge,
  getDefaultConfig,
  mergeConfigs,
} from 'tailwind-merge';

const defaultConfig = getDefaultConfig();

/* ============================================================================
   配置说明:什么需要配置,什么不需要配置
   ============================================================================
   
   ✅ 需要配置的情况:
   
   1. @theme 中定义的非标准名称(如自定义的 font-size、shadow 等)
      例如:--font-size-menu-choice → 生成 text-menu-choice
      虽然会自动生成,但建议配置以确保 tailwind-merge 能识别
   
   2. @utility 定义的组合类(包含多个 CSS 属性)
      例如:@utility text-s-title-s { font-size + font-weight + line-height }
      必须配置!需要创建独立 classGroup + conflictingClassGroups
   
   3. @utility 定义的自定义工具类(如 flex-center)
      例如:@utility flex-center { display + align-items + justify-content }
      需要配置到对应的 classGroup 中
   
   ❌ 不需要配置的情况:
   
   1. @theme 中的标准颜色、间距(会自动生成标准 Tailwind 类)
      例如:--color-brand → 生成 text-brand, bg-brand, border-brand
      例如:--spacing-100 → 生成 p-100, m-100, w-100, h-100
      tailwind-merge 默认就能识别这些标准格式!
   
   2. @apply 定义的组件类(如 .avatar、.btn-primary)
      这些不是工具类,tailwind-merge 会把它们当作普通字符串处理
      不需要配置,也不建议在 tailwind-merge 中使用
   
   ============================================================================ */

// ===== 1. @theme 定义的自定义字体大小 =====
// 对应 CSS: --font-size-menu-choice, --font-size-title-l 等
// 生成的类: text-menu-choice, text-title-l 等
const customFontSizes = [
  'menu-choice',
  'menu-not-choice',
  'title-l',
  'title-m',
  'title-s',
  'number-l',
  'number-m',
  'number-s',
  'body-l',
  'body-m',
  'body-s',
  'tips',
];

// ===== 2. @utility 定义的文字样式组合类 =====
// 对应 CSS: @utility text-s-title-s { font-size + font-weight + line-height }
// 特点: 包含多个 CSS 属性,需要与多个 classGroup 冲突
// 必须配置: 创建独立组 + conflictingClassGroups
const textStyleUtilityClasses = [
  's-title-s',
  's-title-m',
  's-title-l',
  's-body-s',
  's-body-m',
  's-body-l',
];

export const twMergeConfig = mergeConfigs(defaultConfig, {
  extend: {
    classGroups: {
      /* -------------------------------------------------------------------------
         字体大小相关配置
         ------------------------------------------------------------------------- */
      
      // ✅ 需要配置:@theme 中定义的自定义字体大小
      // 对应 CSS: --font-size-menu-choice 等
      // 生成的类: text-menu-choice, text-title-l 等
      'font-size': [
        {
          text: customFontSizes,
        },
      ],
      
      /* -------------------------------------------------------------------------
         组合类配置(包含多个 CSS 属性的 @utility 类)
         ------------------------------------------------------------------------- */
      
      // ✅ 必须配置:@utility 定义的文字样式组合类
      // 对应 CSS: @utility text-s-title-s { font-size + font-weight + line-height }
      // 原因: 包含多个属性,需要与 font-size、font-weight、line-height 都冲突
      'text-style-combo': [
        {
          text: textStyleUtilityClasses,
        },
      ],
      
      /* -------------------------------------------------------------------------
         其他自定义工具类
         ------------------------------------------------------------------------- */
      
      // ✅ 需要配置:@theme 中定义的自定义阴影
      // 对应 CSS: --shadow-d-base, --shadow-d-dropdown 等
      // 生成的类: shadow-d-base, shadow-d-dropdown 等
      shadow: [
        {
          shadow: ['d-base', 'd-dropdown', 'd-button'],
        },
      ],
      
      // ✅ 需要配置:@utility 定义的布局工具类
      // 对应 CSS: @utility flex-center { display + align-items + justify-content }
      // 原因: 自定义类,需要加入 display 组才能与 flex、grid 等冲突
      display: [
        'flex-center',
        'flex-center-x',
        'flex-center-y',
      ],
      
      /* -------------------------------------------------------------------------
         ❌ 不需要配置的内容(tailwind-merge 默认能处理)
         ------------------------------------------------------------------------- */
      
      // ❌ 不需要配置:@theme 中的标准颜色
      // 例如: --color-brand-color → 自动生成 text-brand-color, bg-brand-color 等
      // tailwind-merge 默认能识别所有 text-*, bg-*, border-* 等标准格式
      
      // ❌ 不需要配置:@theme 中的标准间距
      // 例如: --spacing-100 → 自动生成 p-100, m-100, w-100, h-100 等
      // tailwind-merge 默认能识别所有 p-*, m-*, w-*, h-* 等标准格式
      
      // ❌ 不需要配置:@apply 定义的组件类
      // 例如: .avatar, .btn-primary, .card
      // 这些不是工具类,tailwind-merge 会当作普通字符串处理
      // 而且不建议在 tailwind-merge 中使用(见 TAILWIND_MERGE_BEST_PRACTICES.md)
    },
    
    /* ---------------------------------------------------------------------------
       冲突关系配置
       ---------------------------------------------------------------------------
       说明:只有包含多个 CSS 属性的组合类需要配置冲突关系
       例如:text-s-title-s 包含 font-size + font-weight + line-height
       必须声明它与这三个 classGroup 都冲突
       --------------------------------------------------------------------------- */
    conflictingClassGroups: {
      // text-style-combo 与三个样式类都冲突
      'text-style-combo': ['font-size', 'font-weight', 'line-height'],
      
      // 反向声明:让 tailwind-merge 知道双向冲突关系
      'font-size': ['text-style-combo'],
      'font-weight': ['text-style-combo'],
      'line-height': ['text-style-combo'],
    },
  },
});

export const twMerge = createTailwindMerge(() => twMergeConfig);

/**
 * 合并 Tailwind CSS 类名的工具函数
 * 
 * 功能:
 * - 自动合并冲突的 Tailwind 类(后面的覆盖前面的)
 * - 支持条件类名(使用 clsx)
 * - 支持自定义的 @utility 组合类
 * 
 * @example
 * // 基础合并:后面的覆盖前面的
 * cn('px-2 py-1', 'p-3') // => 'p-3'
 * 
 * @example
 * // 条件类名
 * cn('text-red-500', condition && 'text-blue-500')
 * // => 'text-blue-500' (if condition is true)
 * 
 * @example
 * // 组合类之间的合并
 * cn('text-s-title-s', 'text-s-title-l') // => 'text-s-title-l'
 * 
 * @example
 * // 组合类与单独属性类的合并
 * cn('text-2xl font-bold', 'text-s-title-s')
 * // => 'text-s-title-s' (删除了 text-2xl 和 font-bold)
 * 
 * @example
 * // 单独属性类覆盖组合类
 * cn('text-s-title-s', 'font-bold')
 * // => 'font-bold' (删除了 text-s-title-s)
 */
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

这里的自定义配置这一块要根据自己的tailwind来。

导入使用:

import { cn } from "@/utils/cn";
 <div className={cn("grid text-2xl font-bold",'text-s-title-s')}>bbb</div>

五.demo地址

地址:https://gitee.com/rui-rui-an/tailwind-merge

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐