普通视图

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

tailwind-variants基本使用

2026年2月25日 11:08

一.使用场景

1.主要用于C端业务,或者对样式有要求的B端项目
2.公司内部组件库

不适用的场景:
对样式要求不高的管理后台项目,这种项目使用成熟的组件库会让你的开发更加的迅速,样式部分进行样式覆盖即可。

二.为什么要使用tailwind-variants?

之前讲了tailwind和taiwind-merge的使用,但是这两个东西主要是为了写样式,现在要写组件,我们还需要一个利器,那就是tailwind-variants。为什么需要它呢?试想一下,我们需要写一个Button组件,但是这个Button组件有不同的样式,比如空心和实心的。

1.不使用taiwind-variant来封装button组件

import { cn } from "@/utils/cn";
function Button({
  variant = "primary",
  className,
  size = "small",
  children = "button",
  onClick,
}: {
  variant: "primary" | "danger" | "outline" | "disabled";
  className?: string;
  size?: "small" | "medium" | "large";
  children?: React.ReactNode;
  onClick?: () => void;
}) {
  return (
    <button
      onClick={onClick}
      className={cn(
        // 基础样式所有按钮共享)
        "rounded-md cursor-pointer",
        // 变体样式根据不同 variant 应用不同样式)
        {
          "bg-blue-500 text-white": variant === "primary",
          "bg-red-500 text-white": variant === "danger",
          "bg-transparent text-blue-500 border border-blue-500":
            variant === "outline",
          "bg-gray-300 text-gray-500 cursor-not-allowed":
            variant === "disabled",
        },
        {
          "p-2 text-sm": size === "small",
          "p-4 text-base": size === "medium",
          "p-6 text-lg": size === "large",
        },
        {
          // 特殊样式danger  large 组合时添加特殊样式
          'font-bold uppercase border-4': variant === "danger" && size === "large",
        },
        // 外部传入的样式优先级最高可以覆盖上面的样式className
      )}
      disabled={variant === "disabled"}
      >
      {children}
    </button>
  );
}

2.使用taiwind-variant来封装组件

import { tv, type VariantProps } from "tailwind-variants";

const buttonTvStyles = tv({
  base: "rounded-md cursor-pointer",
  variants: {
    color: {
      primary: "bg-blue-500 text-white",
      danger: "bg-red-500 text-white",
      outline: "bg-transparent text-blue-500 border border-blue-500",
      disabled: "bg-gray-300 text-gray-500 cursor-not-allowed",
    },
    size: {
      small: "p-2 text-sm",
      medium: "p-4 text-base",
      large: "p-6 text-lg",
    },
  },
  // ✅ 正确示例:特定组合时添加特殊样式
  compoundVariants: [
    {
      color: "danger",
      size: "large",
      class: "font-bold uppercase border-4", // 特殊样式
    },
  ],
  // 定义默认的样式
  defaultVariants: {
    size: "small",
    color: "primary",
  },
});

// ✅ 完整的组件实现
type ButtonProps = VariantProps<typeof buttonTvStyles> & {
  children?: React.ReactNode;
className?: string; // ✅ 支持外部样式
onClick?: () => void;
  disabled?: boolean; // ✅ 支持禁用
};

function Button2({
  size,
  color,
  children = "button",
  className,
  onClick,
  disabled,
}: ButtonProps) {
  return (
    <button
      onClick={onClick}
      className={buttonTvStyles({ size, color, className })}
      disabled={disabled || color === "disabled"}
      >
      {children}
    </button>
  );
}

使用都是一样的:

export default function TailwindVariants() {
  return (
    <div>
      不同的button样式,如果不用tailwind-variants,这样子写代码:
      <div>
        基础的:
        <Button variant="primary" size="small" className="bg-amber-300" />
        主要的:
        <Button variant="danger" size="medium" />
        空心的:
        <Button variant="outline" size="large" />
        禁用的:
        <Button variant="disabled" />
      </div>
      <div className="mt-5">
        基础的:
        <Button2 color="primary" size="small" />
        主要的:
        <Button2 color="danger" size="medium" />
        空心的:
        <Button2 color="outline" size="large" />
        禁用的:
        <Button2 color="disabled"/>
      </div>
    </div>
  );
}

总结:可以看到,通过使用tailwind-variants可以让我们的代码结构显得非常的清晰,不会杂糅js和css。能通过js判断需要应用的css,使用tailwind-variants都有相对应的方法处理。 其实tailwind-variants最主要的作用就是让我们的代码变得更容易维护,虽然学习成本稍微高一点,但是这些都是值得的

举更多的例子说明-为什么要使用tailwind-variants

1.主要原因还是因为在组件复用样式时,能更好的帮我们处理样式的情况,能让我们的代码结构更加的清晰,而不是css中杂糅着js。
2.能够让我们创建一致性的组件样式。公司组件的风格会相对比较统一

以下为对比使用和没有使用tailwind-variants情况的对比,大家可以看下,就能直观的感受到了

# 📊 为什么需要 tailwind-variants?实际对比

## 🎯 核心问题:当组件变复杂时,`cn` 方案会遇到的三大难题

---

  ## 问题一:复合变体(Compound Variants)难以实现

### 需求场景**特定的变体组合**需要特殊样式时,例如:
  - `danger` + `large` 时:按钮要加粗、大写、加粗边框
  - `primary` + `small` 时:添加阴影效果

### ❌ 使用 cn 的方案(不优雅)

  ```tsx
function Button({ variant, size }) {
  // 需要手动判断组合
  const isDangerLarge = variant === "danger" && size === "large";
  const isPrimarySmall = variant === "primary" && size === "small";
  
  return (
    <button
      className={cn(
        "rounded-md",
        { "bg-red-500": variant === "danger" },
        { "px-6 py-3": size === "large" },
        // ❌ 问题:复合样式散落在条件判断中
        isDangerLarge && "font-bold uppercase border-4",
        isPrimarySmall && "shadow-lg",
      )}
    >
      Button
    </button>
  );
}

问题:

  • ❌ 需要手动定义布尔变量(isDangerLarge
  • ❌ 复合样式混在 cn 函数里,不够清晰
  • ❌ 组合越多,代码越乱
  • ❌ 容易遗漏某些组合

✅ 使用 tailwind-variants 的方案(优雅)

import { tv } from "tailwind-variants";

const button = tv({
base: "rounded-md transition-colors",
variants: {
  variant: {
    primary: "bg-blue-500 text-white",
    danger: "bg-red-500 text-white",
  },
  size: {
    small: "px-2 py-1 text-sm",
    large: "px-6 py-3 text-lg",
  },
},
// ✅ 关键:复合变体集中管理,清晰明了
compoundVariants: [
  {
    variant: "danger",
    size: "large",
    class: "font-bold uppercase border-4 border-red-700",
  },
  {
    variant: "primary",
    size: "small",
    class: "shadow-lg",
  },
],
});

function Button({ variant, size }) {
return <button className={button({ variant, size })}>Button</button>;
}

优势:

  • ✅ 复合变体集中在 compoundVariants 配置中
  • ✅ 代码结构清晰,易于维护
  • ✅ 不会遗漏任何组合
  • ✅ TypeScript 类型安全

问题二:多元素组件(Slots)管理混乱

需求场景

像 Card、Modal、Alert 这种多元素组件,每个子元素都需要根据父组件的变体调整样式。

❌ 使用 cn 的方案(重复且混乱)

function Card({ variant, size }) {
return (
  <div
    className={cn(
      "rounded-lg border",
      // 每个元素都要重复判断 variant  size
      { "bg-white": variant === "default" },
      { "bg-blue-50": variant === "highlighted" },
      { "p-4": size === "small" },
      { "p-6": size === "large" }
    )}
  >
    <h3
      className={cn(
        "font-bold",
        // ❌ 问题又要写一遍 variant 判断
        { "text-gray-900": variant === "default" },
        { "text-blue-900": variant === "highlighted" },
        // ❌ 问题又要写一遍 size 判断
        { "text-lg": size === "small" },
        { "text-2xl": size === "large" }
      )}
    >
      Title
    </h3>
    <p
      className={cn(
        // ❌ 问题第三次重复写 variant  size 判断...
        { "text-gray-600": variant === "default" },
        { "text-blue-600": variant === "highlighted" },
        { "text-sm": size === "small" },
        { "text-base": size === "large" }
      )}
    >
      Content
    </p>
  </div>
);
}

问题:

  • ❌ 每个子元素都要重复写条件判断
  • ❌ 代码重复严重,难以维护
  • ❌ 修改变体时需要改多处
  • ❌ 容易出错(忘记更新某个元素)

✅ 使用 tailwind-variants 的方案(Slots)

import { tv } from "tailwind-variants";

const card = tv({
// ✅ 关键:使用 slots 管理多个元素
slots: {
  base: "rounded-lg border",
  title: "font-bold",
  content: "mt-2",
},
variants: {
  variant: {
    default: {
      base: "bg-white border-gray-200",
      title: "text-gray-900",
      content: "text-gray-600",
    },
    highlighted: {
      base: "bg-blue-50 border-blue-300",
      title: "text-blue-900",
      content: "text-blue-600",
    },
  },
  size: {
    small: {
      base: "p-4",
      title: "text-lg",
      content: "text-sm",
    },
    large: {
      base: "p-6",
      title: "text-2xl",
      content: "text-base",
    },
  },
},
});

function Card({ variant, size }) {
// ✅ 一次性获取所有元素的样式
const { base, title, content } = card({ variant, size });

return (
  <div className={base()}>
    <h3 className={title()}>Title</h3>
    <p className={content()}>Content</p>
  </div>
);
}

优势:

  • ✅ 所有元素的变体样式集中管理
  • ✅ 代码清晰,结构化
  • ✅ 修改变体只需改一处
  • ✅ 不会遗漏任何元素

问题三:响应式变体

需求场景

按钮在不同屏幕尺寸下需要不同的样式。

❌ 使用 cn 的方案(需要手动写响应式类)

function Button({ variant }) {
  return (
    <button
      className={cn(
        "p-2",
        // ❌ 需要手动写响应式前缀
        "md:p-4 lg:p-6",
        {
          "bg-blue-500 md:bg-red-500 lg:bg-green-500": variant === "primary",
        }
      )}
    >
      Button
    </button>
  );
}

✅ 使用 tailwind-variants 的方案(原生支持)

const button = tv({
  variants: {
    variant: {
      primary: "bg-blue-500",
      danger: "bg-red-500",
    },
  },
});

// ✅ 可以这样传入响应式变体
<button className={button({ 
  variant: { 
    initial: "primary",  // 默认
    md: "danger",        // 中等屏幕
    lg: "primary"        // 大屏幕
  } 
})}>
  Button
</button>

问题四:代码可维护性

场景对比:一个真实的按钮组件

假设需要支持:

  • 3 个颜色(primary、danger、success)
  • 3 个尺寸(sm、md、lg)
  • 2 个圆角(normal、full)
  • 是否有阴影
  • 复合变体:danger + lg 时要加粗

❌ 使用 cn 的代码量

function Button({ variant, size, rounded, shadow }) {
  const isDangerLarge = variant === "danger" && size === "lg";
  
  return (
    <button
      className={cn(
        "transition-colors font-medium",
        // variant (3 )
        {
          "bg-blue-500 text-white": variant === "primary",
          "bg-red-500 text-white": variant === "danger",
          "bg-green-500 text-white": variant === "success",
        },
        // size (3 )
        {
          "px-2 py-1 text-sm": size === "sm",
          "px-4 py-2 text-base": size === "md",
          "px-6 py-3 text-lg": size === "lg",
        },
        // rounded (2 )
        {
          "rounded-md": rounded === "normal",
          "rounded-full": rounded === "full",
        },
        // shadow
        shadow && "shadow-lg",
        // 复合变体
        isDangerLarge && "font-bold border-4"
      )}
    >
      Button
    </button>
  );
}

总计:约 30 行,且结构混乱

✅ 使用 tailwind-variants 的代码量

const button = tv({
  base: "transition-colors font-medium",
  variants: {
    variant: {
      primary: "bg-blue-500 text-white",
      danger: "bg-red-500 text-white",
      success: "bg-green-500 text-white",
    },
    size: {
      sm: "px-2 py-1 text-sm",
      md: "px-4 py-2 text-base",
      lg: "px-6 py-3 text-lg",
    },
    rounded: {
      normal: "rounded-md",
      full: "rounded-full",
    },
    shadow: {
      true: "shadow-lg",
    },
  },
  compoundVariants: [
    {
      variant: "danger",
      size: "lg",
      class: "font-bold border-4",
    },
  ],
  defaultVariants: {
    variant: "primary",
    size: "md",
    rounded: "normal",
  },
});

function Button(props) {
  return <button className={button(props)}>Button</button>;
}

总计:约 35 行,但结构清晰、可维护性强


📊 总结对比

特性 cn 方案 tailwind-variants
简单变体 ✅ 够用 ✅ 优雅
复合变体 ❌ 需要手动判断 compoundVariants
多元素组件 ❌ 代码重复 slots 统一管理
响应式变体 ⚠️ 手动写前缀 ✅ 原生支持
代码可读性 ⚠️ 变体多时混乱 ✅ 结构化配置
维护成本 ⚠️ 修改需要改多处 ✅ 集中管理
TypeScript ⚠️ 手动定义 ✅ 自动推断
学习曲线 ✅ 简单 ⚠️ 需要学习 API

🎯 结论

适合继续用 cn 的场景:

  • ✅ 简单组件(1-2 个变体维度)
  • ✅ 不需要复合变体
  • ✅ 不需要多元素协同管理
  • ✅ 原型开发、快速迭代

建议使用 tailwind-variants 的场景:

  • ✅ 构建设计系统或组件库
  • ✅ 组件有复杂的变体组合需求
  • ✅ 多元素组件(Card、Modal、Alert 等)
  • ✅ 需要响应式变体
  • ✅ 团队协作,需要统一规范
  • ✅ 长期维护的项目

核心差异: cn 是战术工具(解决单个组件),tailwind-variants 是战略工具(解决整个设计系统)。




## 三.学习文档
直接到[官网](https://www.tailwind-variants.org/docs/getting-started)去学习就好了,它的官网的例子写的真的很详细。跟着官网的例子走一遍,基本上就知道怎么用了  
官网:[https://www.tailwind-variants.org/docs/getting-started](https://www.tailwind-variants.org/docs/getting-started)

## 四.怎么使用-基础组件?
### 1.安装 
```markdown
npm install tailwind-variants

2.组件中使用tailwind-variants

import { tv } from "tailwind-variants";

export default function Button({
  size,
  color,
  children,
}: {
  size?: "sm" | "md" | "lg";
  color?: "primary" | "secondary";
  children?: React.ReactNode;
}) {
  const buttonTvStyles = tv({
    base: "font-medium bg-blue-500 text-white rounded-full active:opacity-80",
    variants: {
      color: {
        primary: "bg-blue-500 text-white",
        secondary: "bg-purple-500 text-white",
      },
      size: {
        sm: "text-sm",
        md: "text-base",
        lg: "px-4 py-3 text-lg",
      },
    },
    compoundVariants: [
      {
        size: ["sm", "md"],
        class: "px-3 py-1",
      },
    ],
    defaultVariants: {
      size: "md",
      color: "primary",
    },
  });

  return (
    <button className={buttonTvStyles({ size, color })}>{children}</button>
  );
}

3.使用组件

import Button from "./components/Button";
export default function UseTailwindVariants() {
  return (
    <div>
      <Button size="sm" color="secondary">组件按钮</Button>
    </div>
  );
}

4.解释tv中定义的各属性的作用:

就以上面封装的button组件的代码为例:

a. base:

作用:定义基础样式,该样式会和变体中的样式一起应用到组件上,这里的样式定义一般就放最基础的,需要改动的样式放到变体variants中去

b.variants(变体):

作用:定义变体,可以定义不同样式,不同大小的按钮(也可以定义其他的,比如圆角,阴影等等)
文档:www.tailwind-variants.org/docs/varian…

解释下,比如

variants: {
  color: {
    primary: "bg-blue-500 text-white",
    secondary: "bg-purple-500 text-white",
  },
  size: {
    sm: "text-sm",
    md: "text-base",
    lg: "px-4 py-3 text-lg",
  },
},

意思是这里我们定义了两个变体,一个是color风格,一个是size的大小。color分为primary和secondary两种风格,size主要分为三个大小。 这个color和size主要是可以通过外面传进来的变量来让我们的组件确认是渲染哪一种,如:

<Button size="sm" color="secondary">组件按钮</Button>
<Button size="sm" color="primary">组件按钮</Button>
<Button size="md" color="primary">组件按钮</Button>

我还可以这样子定义:

variants: {
  radius: {
    none: "rounded-none",
      sm: "rounded-sm",
      md: "rounded-md",
      lg: "rounded-lg",
      full: "rounded-full",
      },
  shadow: {
    none: "shadow-none",
      sm: "shadow-sm",
      md: "shadow-md",
      lg: "shadow-lg",
      },
},

这样子就又加了两种变体,radius和shadow,radius圆角有五种风格的圆角,shadow阴影有四种风格的阴影。使用的时候就可以传进来使用了:

<Button size="md" color="primary" radius="full">组件按钮</Button>
<Button size="md" color="primary" shadow="md" >组件按钮</Button>

注意:定义变体时是三层结构的object,先是variants变体对象,其次是变体名称对应的对象,最后是变体名称对应有哪些值,并且这些值对应的样式是什么

c.compoundVariants(复合变体):

作用:定义复合样式,即满足该条件的会应用该样式
比如

compoundVariants: [
  {
    size: ["sm", "md"],
    class: "px-3 py-1",
  },
],

这个的意思是:size为sm或者md的组件会应用下面的class(即'px-3 py-1')
我还可以这样子定义:

compoundVariants: [
  {
    color: 'primary',
    size: 'sm',
    class: 'bg-red-500 text-white',
  }
],

这个的意思是:当color为primary且size为sm的组件会应用'bg-red-500 text-white'的样式

注意别理解错了:

数组 ["sm", "md"] = 或关系 (满足其中之一)

多个属性 = 且关系 (必须同时满足)

d.defaultVariants(默认变体)

作用:定义默认变体(即不传的时候会默认渲染的)

比如

defaultVariants: {
  size: "md",
  color: "primary",
},

如果这个button不传size和color,那么它的默认样式就是size为md,color为primary

五.核心:variants(变体)

文档:www.tailwind-variants.org/docs/varian…

a.基础variants: 例如:

variants: {
    color: {
      primary: 'bg-blue-500 hover:bg-blue-700',
      secondary: 'bg-purple-500 hover:bg-purple-700',
      success: 'bg-green-500 hover:bg-green-700'
    }
  }

这里只定义了一种color的变体,这种变体有三种选择,分别是primary,secondary,success,对应了不同的样式

使用变体:

  <Button color="secondary">组件按钮</Button>
  <Button color="primary">组件按钮</Button>

b.多个变体

variants: {
  color: {
    primary: 'bg-blue-500 hover:bg-blue-700',
    secondary: 'bg-purple-500 hover:bg-purple-700',
    success: 'bg-green-500 hover:bg-green-700'
  },
  size: {
    sm: 'py-1 px-3 text-xs',
    md: 'py-1.5 px-4 text-sm',
    lg: 'py-2 px-6 text-md'
  }
}

这里定义了两种变体,color可以选择primary,secondary,success, size可以选择sm,md, lg

使用变体

  <Button size="sm" color="secondary">组件按钮</Button>
  <Button size="sm" color="primary">组件按钮</Button>

c.boolean变体

variants: {
  color: {
    primary: 'bg-blue-500 hover:bg-blue-700',
    secondary: 'bg-purple-500 hover:bg-purple-700',
    success: 'bg-green-500 hover:bg-green-700'
  },
  disabled: {
    true: 'opacity-50 bg-gray-500 pointer-events-none'
  }
}

这里定义了disabled的boolean变体,当disabled为true时会应用opacity-50 bg-gray-500 pointer-events-none这些样式

封装的时候是这么写的:

import { tv } from "tailwind-variants";

export default function Button({
  size,
  color,
  radius,
  shadow,
  children,
  disabled
}: {
  size?: "sm" | "md" | "lg";
  color?: "primary" | "secondary";
  radius?: "none" | "sm" | "md" | "lg" | "full";
  shadow?: "none" | "sm" | "md" | "lg";
  children?: React.ReactNode;
  disabled?: boolean
}) {
  const buttonTvStyles = tv({
    base: "font-medium bg-blue-500 text-white active:opacity-80 cursor-pointer",
    variants: {
      color: {
        primary: "bg-blue-500 text-white",
        secondary: "bg-purple-500 text-white",
      },
      size: {
        sm: "text-sm",
        md: "text-base",
        lg: "px-4 py-3 text-lg",
      },
      radius: {
        none: "rounded-none",
        sm: "rounded-sm",
        md: "rounded-md",
        lg: "rounded-lg",
        full: "rounded-full",
      },
      shadow: {
        none: "shadow-none",
        sm: "shadow-sm",
        md: "shadow-md",
        lg: "shadow-lg",
      },
      disabled: {
        true: 'opacity-50 bg-gray-500 pointer-events-none'
      }
    },
    compoundVariants: [
      {
        size: ["sm", "md"],
        class: "px-3 py-1",
      },
      {
        color: 'primary',
        size: 'sm',
        class: 'bg-red-500 text-white',
      }
    ],
    defaultVariants: {
      size: "md",
      color: "primary",
      radius: "md",
      shadow: "none",
    },
  });

  return (
    <button className={buttonTvStyles({ size, color, radius, shadow, disabled })}>
      {children}
    </button>
  );
}

使用boolean变体

<Button size="md" color="primary" disabled>组件按钮</Button>

d.Compound variants(复合变体)

这个在上面说过了,就不举例子了

e.Default variants(默认变体)

这个也在上面说过了,就不举例子了

六.核心:slots(插槽)

文档:www.tailwind-variants.org/docs/slots

上面的例子是拿了一个按钮作为例子的,但是我们实际业务的组件绝对不只是这么简单,可能很复杂有一堆的元素,那么就要引入tailwind-variants的另一个重要的属性了-----slots,这个属性能让我们对不同的盒子进行自定义样式,以下拿例子来说明:

a.基础使用:

import { tv } from "tailwind-variants";
import avatar2 from "./intro-avatar.webp";

export default function Card() {
  const card = tv({
    slots: {
      base: "bg-slate-100 rounded-xl p-8 md:p-0",
      avatar:
        "w-24 h-24 md:h-auto md:rounded-none rounded-full mx-auto drop-shadow-lg",
      wrapper: "flex-1 pt-6 md:p-8 text-center md:text-left space-y-4",
      description: "text-md font-medium",
      infoWrapper: "font-medium",
      name: "text-sm text-sky-500",
      role: "text-sm text-slate-700",
    },
  });

  const { base, avatar, wrapper, description, infoWrapper, name, role } =
    card();

  return (
    <figure className={base()}>
      <img
        className={avatar()}
        src={avatar2}
        alt=""
        width="384"
        height="512"
      />
      <div className={wrapper()}>
        <blockquote>
          <p className={description()}>
            “Tailwind variants allows you to reduce repeated code in your
            project and make it more readable. They fixed the headache of
            building a design system with TailwindCSS.”
          </p>
        </blockquote>
        <figcaption className={infoWrapper()}>
          <div className={name()}>Zoey Lang</div>
          <div className={role()}>Full-stack developer, HeroUI</div>
        </figcaption>
      </div>
    </figure>
  );
}

这里通过slots插槽定义了组件的多个部分的样式,分为了base, avatar, wrapper, description, infoWrapper, name, role,其实就是相当于起了个类名,然后各个类名的样式对应什么。

b.和变体variants一起使用

就是既要有插槽,又有变体的时候要怎么使用?
这里我给出来了格式,但是没有写具体的样式

variants: {
  color: {
    primary: {
      base: "xxx bbb",
      avatar:'ccc ddd'
    },
  },
},

注意:这里变成了四层结构的object,第一层依然是variants变体对象,第二层还是定义的变体名称对象,第三层不再是值和值对应的样式了,而是值和值对应的object,这个object包括的是各个slot名称和要加上去的样式。
这个意思就是说,当这个Card组件的color为primary时, 会将'xxx bbb'的样式应用到base盒子上去,'ccc ddd'的样式会被应用到avatar盒子上去

比如,我现在定义了三种color的样式,在不同的color下,description和role的字体颜色不同

variants: {
  color: {
    default: {
      description: "text-gray-900",
      role: "text-gray-900",
    },
    primary: {
      description: "text-green-500",
      role: "text-green-500",
    },
    danger: {
      description: "text-red-500",
      role: "text-red-500",
    },
  },
},

当我使用时,就可以传入这个color以实现不同的效果:

<Card color="default"/>
<Card color="primary"/>

c.和compoundVariants(复合变体)一起使用

这里我直接给出我的代码

compoundVariants:[
  {
    color:'primary',
    class:{
      description:'text-blue-500',
      avatar:'rounded-full md:rounded-full'
    }
  }
],

解释下,这里的color的值,可以选择你上面变体定义的三个值default,primary,danger
我这里的复合变体的意思是:如果你的color为primary,那么description的盒子再应用text-blue-500的样式(之前的),avatar的盒子再应用rounded-full md:rounded-full的样式

d.Compoundslots(复合slot)

这个属性和compoundVariants的用法是基本一致的,比如

compoundSlots:[
  {
    slots:['role','description'],
    color:'danger',
    class: 'bg-red-200'
  }
],

这个的代码的意思是: slots为role和description的盒子,并且该组件的color变体为danger时, 会应用bg-red-200的class,如图,现在只有danger的组件有红色背景

e.Slot variant overrides(这个相对难以理解,但是用的也比较少)

官网例子:www.tailwind-variants.org/docs/slots#…
这里我用组件的方式改写了一下

import { tv } from "tailwind-variants";

// ✅ 定义 Item 类型
type TabItem = {
  id: string;
  label: string;
  color?: "primary" | "secondary";
  isSelected?: boolean;
};

// ✅ 正确的 props 类型定义
export default function Tab({ items }: { items: TabItem[] }) {
  const card = tv({
    slots: {
      base: "flex gap-2",
      tab: "rounded px-4 py-2 cursor-pointer transition-all",
    },
    variants: {
      color: {
        primary: {
          tab: "text-blue-500 dark:text-blue-400",
        },
        secondary: {
          tab: "text-purple-500 dark:text-purple-400",
        },
      },
      isSelected: {
        true: {
          tab: "font-bold bg-blue-100",
        },
        false: {
          tab: "font-normal bg-gray-50",
        },
      },
    },
  });

  const { base, tab } = card({ color: "primary" });

  return (
    <div className={base()}>
      {items.map((item) => (
        <div 
          key={item.id}
          className={tab({ isSelected: item.isSelected, color: item.color })} 
          id={item.id}
        >
          {item.label}
        </div>
      ))}
    </div>
  );
}

传入使用:

const tabItems = [
  { id: "1", label: "Tab 1", isSelected: true },
  { id: "2", label: "Tab 2", isSelected: false },
  { id: "3", label: "Tab 3", isSelected: false, color: "secondary" as const },
];
<Tab items={tabItems} />

效果:

解释一下: 这个tab这里本来默认是渲染rounded px-4 py-2 cursor-pointer transition-all的样式的,但是我们可以通过把变体传进来,进而去覆盖默认的tab的样式。
比如{ id: "1", label: "Tab 1", isSelected: true } 就会把font-bold bg-blue-100的样式加到tab的样式里面去。
比如{ id: "3", label: "Tab 3", isSelected: false, color: "secondary" as const }会把secondary: {

   tab: "text-purple-500 dark:text-purple-400",

}的样式加入到slots中的tab中去

七.Overriding styles(样式覆盖)

文档:www.tailwind-variants.org/docs/overri…

a.单组件(组件里只有一个元素)样式覆盖

简单叙述就是:传入一个className,然后在buttonTvStyles调用时作为参数传入进去

import { tv } from "tailwind-variants";

export default function Button({
  size,
  color,
  radius,
  shadow,
  children,
  disabled,
  className
}: {
  size?: "sm" | "md" | "lg";
  color?: "primary" | "secondary";
  radius?: "none" | "sm" | "md" | "lg" | "full";
  shadow?: "none" | "sm" | "md" | "lg";
  children?: React.ReactNode;
  disabled?: boolean;
  className?: string;
}) {
  const buttonTvStyles = tv({
    base: "font-medium bg-blue-500 text-white active:opacity-80 cursor-pointer",
    variants: {
      color: {
        primary: "bg-blue-500 text-white",
        secondary: "bg-purple-500 text-white",
      },
      size: {
        sm: "text-sm",
        md: "text-base",
        lg: "px-4 py-3 text-lg",
      },
      radius: {
        none: "rounded-none",
        sm: "rounded-sm",
        md: "rounded-md",
        lg: "rounded-lg",
        full: "rounded-full",
      },
      shadow: {
        none: "shadow-none",
        sm: "shadow-sm",
        md: "shadow-md",
        lg: "shadow-lg",
      },
      disabled: {
        true: 'opacity-50 bg-gray-500 pointer-events-none'
      }
    },
    compoundVariants: [
      {
        size: ["sm", "md"],
        class: "px-3 py-1",
      },
      {
        color: 'primary',
        size: 'sm',
        class: 'bg-red-500 text-white',
      }
    ],
    defaultVariants: {
      size: "md",
      color: "primary",
      radius: "md",
      shadow: "none",
    },
  });

  return (
    <button className={buttonTvStyles({ size, color, radius, shadow, disabled, className })}>
      {children}
    </button>
  );
}

使用:

<Button size="md" color="secondary" className="bg-pink-500 hover:bg-pink-600">
  覆盖为粉色
</Button>

b.带插槽组件(组件里有多个元素)的样式覆盖

import { tv } from "tailwind-variants";
import avatar2 from "./intro-avatar.webp";

export default function Card({
  color,
  classNames
}: { 
  color?: "default" | "primary" | "danger";
  classNames?: {
    base?: string;
    avatar?: string;
    wrapper?: string;
    description?: string;
    infoWrapper?: string;
    name?: string;
    role?: string;
  };
}) {
  const card = tv({
    slots: {
      base: "bg-slate-100 rounded-xl p-8 md:p-0",
      avatar:
        "w-24 h-24 md:h-auto md:rounded-none rounded-full mx-auto drop-shadow-lg",
      wrapper: "flex-1 pt-6 md:p-8 text-center md:text-left space-y-4",
      description: "text-md font-medium",
      infoWrapper: "font-medium",
      name: "text-sm text-sky-500",
      role: "text-sm text-slate-700",
    },
    variants: {
      color: {
        default: {
          description: "text-gray-900",
          role: "text-gray-900",
        },
        primary: {
          description: "text-green-500",
          role: "text-green-500",
        },
        danger: {
          description: "text-red-500",
          role: "text-red-500",
        },
      },
    },
    compoundVariants:[
      {
        color:'primary',
        class:{
          description:'text-blue-500',
          avatar:'rounded-full md:rounded-full'
        }
      }
    ],
    compoundSlots:[
      {
        slots:['role','description'],
        color:'danger',
        class: 'bg-red-200'
      }
    ],
    defaultVariants:{
        color:'default'
    }
  });

  const { base, avatar, wrapper, description, infoWrapper, name, role } =
    card({ color });

  return (
    <figure className={base({ class: classNames?.base })}>
      <img className={avatar({ class: classNames?.avatar })} src={avatar2} alt="" width="384" height="512" />
      <div className={wrapper({ class: classNames?.wrapper })}>
        <blockquote>
          <p className={description({ class: classNames?.description })}>
            “Tailwind variants allows you to reduce repeated code in your
            project and make it more readable. They fixed the headache of
            building a design system with TailwindCSS.”
          </p>
        </blockquote>
        <figcaption className={infoWrapper({ class: classNames?.infoWrapper })}>
          <div className={name({ class: classNames?.name })}>Zoey Lang</div>
          <div className={role({ class: classNames?.role })}>Full-stack developer, HeroUI</div>
        </figcaption>
      </div>
    </figure>
  );
}

使用:

{/* 综合覆盖多个样式 */}
<Card 
  color="danger"
  classNames={{
    base: "bg-yellow-50 border-2 border-yellow-400",
    avatar: "grayscale hover:grayscale-0 transition-all",
    wrapper: "bg-yellow-100 rounded-lg",
    description: "text-yellow-900 italic",
    name: "text-yellow-700 font-bold",
    role: "text-yellow-600"
  }}
/>

八.继承

文档:www.tailwind-variants.org/docs/compos…
主要就是可以通过extend来继承别的组件的样式。 也可以使用base,slots或者variants来组合样式,这里具体就不举例了,因为比较用法比较简单。

九.结合TS

结合ts使用。

import { tv, type VariantProps } from 'tailwind-variants';

export const button = tv({
  base: 'px-4 py-1.5 rounded-full hover:opacity-80',
  variants: {
    color: {
      primary: 'bg-blue-500 text-white',
      neutral: 'bg-zinc-500 text-black dark:text-white'
    },
    flat: {
      true: 'bg-transparent'
    }
  },
  defaultVariants: {
    color: 'primary'
  },
  compoundVariants: [
    {
      color: 'primary',
      flat: true,
      class: 'bg-blue-500/40'
    },
    {
      color: 'neutral',
      flat: true,
      class: 'bg-zinc-500/20'
    }
  ]
});

/**
 * Result:
 * color?: "primary" | "neutral"
 * flat?: boolean
 */

type ButtonVariants = VariantProps<typeof button>;

interface ButtonProps extends ButtonVariants {
  children: React.ReactNode;
}

export const Button = (props: ButtonProps) => {
  return <button className={button(props)}>{props.children}</button>;
};
import { tv,VariantProps } from "tailwind-variants";
import avatar2 from "../../UseTailwindVariants/components/intro-avatar.webp";

const card = tv({
  slots: {
    base: "bg-slate-100 rounded-xl p-8 md:p-0",
    avatar:
      "w-24 h-24 md:h-auto md:rounded-none rounded-full mx-auto drop-shadow-lg",
    wrapper: "flex-1 pt-6 md:p-8 text-center md:text-left space-y-4",
    description: "text-md font-medium",
    infoWrapper: "font-medium",
    name: "text-sm text-sky-500",
    role: "text-sm text-slate-700",
  },
  variants: {
    color: {
      default: {
        description: "text-gray-900",
        role: "text-gray-900",
      },
      primary: {
        description: "text-green-500",
        role: "text-green-500",
      },
      danger: {
        description: "text-red-500",
        role: "text-red-500",
      },
    },
  },
  compoundVariants:[
    {
      color:'primary',
      class:{
        description:'text-blue-500',
        avatar:'rounded-full md:rounded-full'
      }
    }
  ],
  compoundSlots:[
    {
      slots:['role','description'],
      color:'danger',
      class: 'bg-red-200'
    }
  ],
  defaultVariants:{
      color:'default'
  }
});

interface CardVariants extends VariantProps<typeof card> {
  classNames?: {
    base?: string;
    avatar?: string;
    wrapper?: string;
    description?: string;
    infoWrapper?: string;
    name?: string;
    role?: string;
  };
}


export function Card(props:CardVariants) {
  const { color, classNames } = props;

  const { base, avatar, wrapper, description, infoWrapper, name, role } =
    card({ color });

  return (
    <figure className={base({ class: classNames?.base })}>
      <img className={avatar({ class: classNames?.avatar })} src={avatar2} alt="" width="384" height="512" />
      <div className={wrapper({ class: classNames?.wrapper })}>
        <blockquote>
          <p className={description({ class: classNames?.description })}>
            “Tailwind variants allows you to reduce repeated code in your
            project and make it more readable. They fixed the headache of
            building a design system with TailwindCSS.”
          </p>
        </blockquote>
        <figcaption className={infoWrapper({ class: classNames?.infoWrapper })}>
          <div className={name({ class: classNames?.name })}>Zoey Lang</div>
          <div className={role({ class: classNames?.role })}>Full-stack developer, HeroUI</div>
        </figcaption>
      </div>
    </figure>
  );
}

多组件结合ts使用是优化,主要是优化传参:

interface CardVariants extends VariantProps<typeof card> {
  classNames?: {
    base?: string;
    avatar?: string;
    wrapper?: string;
    description?: string;
    infoWrapper?: string;
    name?: string;
    role?: string;
  };
}

还有这里要优化,因为每次都要写:

<figure className={base({ class: classNames?.base })}>
      <img className={avatar({ class: classNames?.avatar })} src={avatar2} alt="" width="384" height="512" />
      <div className={wrapper({ class: classNames?.wrapper })}>
        <blockquote>
          <p className={description({ class: classNames?.description })}>
            “Tailwind variants allows you to reduce repeated code in your
            project and make it more readable. They fixed the headache of
            building a design system with TailwindCSS.”
          </p>
        </blockquote>
        <figcaption className={infoWrapper({ class: classNames?.infoWrapper })}>
          <div className={name({ class: classNames?.name })}>Zoey Lang</div>
          <div className={role({ class: classNames?.role })}>Full-stack developer, HeroUI</div>
        </figcaption>
      </div>
    </figure>

最后的优化版本:

import { applyStyles, tv, type TVProps } from './tv';
import avatar2 from "../../UseTailwindVariants/components/intro-avatar.webp";

const createStyle = tv({
  slots: {
    base: "bg-slate-100 rounded-xl p-8 md:p-0",
    avatar:
      "w-24 h-24 md:h-auto md:rounded-none rounded-full mx-auto drop-shadow-lg",
    wrapper: "flex-1 pt-6 md:p-8 text-center md:text-left space-y-4",
    description: "text-md font-medium",
    infoWrapper: "font-medium",
    name: "text-sm text-sky-500",
    role: "text-sm text-slate-700",
  },
  variants: {
    color: {
      default: {
        description: "text-gray-900",
        role: "text-gray-900",
      },
      primary: {
        description: "text-green-500",
        role: "text-green-500",
      },
      danger: {
        description: "text-red-500",
        role: "text-red-500",
      },
    },
  },
  compoundVariants: [
    {
      color: 'primary',
      class: {
        description: 'text-blue-500',
        avatar: 'rounded-full md:rounded-full'
      }
    }
  ],
  compoundSlots: [
    {
      slots: ['role', 'description'],
      color: 'danger',
      class: 'bg-red-200'
    }
  ],
  defaultVariants: {
    color: 'default'
  }
});


type Props = TVProps<typeof createStyle>;

export function Card(props: Props) {
  const { variants, classNames } = props;
  const styles = applyStyles(createStyle, {
    variants,
    classNames
  });

  const { base, avatar, wrapper, description, infoWrapper, name, role } = styles;

  return (
    <figure className={base()}>
      <img className={avatar()} src={avatar2} alt="" width="384" height="512" />
      <div className={wrapper()}>
        <blockquote>
          <p className={description()}>
            “Tailwind variants allows you to reduce repeated code in your
            project and make it more readable. They fixed the headache of
            building a design system with TailwindCSS.”
          </p>
        </blockquote>
        <figcaption className={infoWrapper()}>
          <div className={name()}>Zoey Lang</div>
          <div className={role()}>Full-stack developer, HeroUI</div>
        </figcaption>
      </div>
    </figure>
  );
}
import clsx from 'clsx';
import { createTV, VariantProps } from 'tailwind-variants';

import { twMergeConfig } from '@/utils/cn';

export const tv = createTV({
  twMerge: true,
  twMergeConfig,
});

type SlotClassNames<T> = T extends {
  slots: infer S;
}
  ? SlotClassNames<S>
  : T extends (config: infer C) => any
    ? SlotClassNames<C>
    : Partial<Record<keyof T, string>>;

export interface TVProps<T extends (...args: any) => any> {
  classNames?: SlotClassNames<T>;
  variants?: VariantProps<T>;
}

/**
 * A utility for applying variants and custom classNames to styles created with `tailwind-variants`.
 * It allows a component to accept a `classNames` prop to override or extend the styles of its internal slots.
 *
 * @param createStyles - The style function created by `tv` from `tailwind-variants`.
 * @param props - The component's props, expected to contain `variants` and `classNames`.
 * @returns The resolved styles object, where each slot function is wrapped to include the custom classNames.
 *
 * @example
 * const buttonStyles = tv({ slots: { base: '...', icon: '...' } });
 * const props = { variants: { color: 'primary' }, classNames: { icon: 'text-red-500' } };
 * const styles = applyStyles(buttonStyles, props);
 * <div class={styles.base()}><span class={styles.icon()} /></div>
 * The icon will have the 'text-red-500' class applied.
 */
export function applyStyles<T extends (...args: any) => any>(
  createStyles: T,
  props: TVProps<T>,
): ReturnType<T> {
  const styles = createStyles(props.variants);

  Object.keys(styles).forEach((key) => {
    const original = styles[key];
    // Wrap the original slot function to merge the `classNames` prop.
    styles[key] = (args: any) => {
      return original({
        ...args,
        className: clsx(args?.className, props.classNames?.[key as never]),
      });
    };
  });

  return styles;
}

TVProps的作用是:将传入进来的slot类型分别处理成一个对象,包含了classNames和variants属性,这两个对象里面包含了slot类型的属性。这样子就组成了组件的类型,而不用每次都写这么多了。

applyStyles函数的作用是,传入原来的style实例和props对象(包含variants和classNames属性),返回新的一个style实例。作用是:把外部传入的 classNames 自动合并到每个 slot 中,省去在 JSX 里逐个写 base({ class: classNames?.base }) 的重复代码。

const styles = applyStyles(createStyle, {
  variants,
  classNames
});

十:自定义配置。

同样的,tailwind-variants无法识别tailwindcss以外的自定义的类名,所以我们需要把自定义配置导出过来,在创建tv的时候将该自定义配置传入进去。

import { createTV, VariantProps } from 'tailwind-variants';
import { twMergeConfig } from '@/utils/cn';

export const tv = createTV({
  twMerge: true,
  twMergeConfig,
});

所以在使用ts的时候要从tv.ts中导入了
原来:
import { tv,VariantProps } from "tailwind-variants";
现在:
import { tv } from './tv';

十一:地址
demo地址:gitee.com/rui-rui-an/…

tailwind-merge的基本使用

2026年2月25日 11:05

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

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

官方文档:tailwindcss.com/docs/stylin…

有一个例子是:

<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-3 的 12px 全方向?
因为在 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.坑点

文档:github.com/dcastil/tai…
虽然这样子使用很方便,但是它有个坑点,就是之前我们也提到过,我们自己在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地址

地址:gitee.com/rui-rui-an/…

tailwindcss v4的基础使用

2026年2月25日 11:03

一:前言

为什么要使用tailwindcss? 主要是因为可以减少命名和选择器的烦恼,不用去定义class类名了,每次要定义类名都想的头疼。然后使用tailwindcss来开发,可以减少 CSS 文件大小,只生成实际使用的样式,通过 PurgeCSS 移除未使用的 CSS,生产环境文件体积极小。最后就是内置了响应式设计,内置响应式前缀(sm:, md:, lg:, xl:, 2xl:,可以轻松创建移动优先的响应式布局。

二:安装

参考文档:tailwindcss.com/docs/instal…

我这边使用的是vite,那么就跟着vite的安装文档来走就行了(最好大家跟着文档来走):

a.安装命令:

npm install tailwindcss @tailwindcss/vite

b.在vite.config.ts中配置它

import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    tailwindcss(),
  ],
})

c.导入

@import "tailwindcss";

d.在main.tsx中导入它:

注意把normalize.css和reset.css都可以干掉了,因为tailwindcss里面内置了基础样式重置系统

e.插件安装:

使用tailwindcss一定一定要安装插件,因为tailwindcss的内置样式实在太多了,不需要记也记不得这么全
这里我们使用:Tailwind CSS IntelliSense


这样子写的时候就有提示了:

三:使用

安装完成之后,就可以这样子使用它了:

四:一些常用的css样式在tailwind中的写法

a.最常用的margin和padding:

p-4:设置元素的内边距(padding)为 1rem,相当于:padding: 16px。

m-8:设置元素的外边距(margin)为 2rem, 相当于: padding: 32px。

mt-4:设置元素的上外边距为 1rem,相当于: margin-top: 16px。

my-1: 相当于:margin-top:4px和margin-bottom:4px的组合。

mx-1: 相当于:margin-left:4px和margin-right:4px的组合。

注意这里的单位是rem,如果不去更改html的font-size的大小话,一般就是1rem=16px

所以在不更改大小的前提下 p-1,就代表设置内边距为4px。

但是有个问题:就是上面很多都是预设值,但是ui有时候给的没有这个值,要使用自定义的值怎么办?

那么就用mt-[188px]这种格式:这就代表了:margin-top:188px。所有需要自定义的,都是使用[]这个来自定义,记得要带上单位

注意:这里一定要学会看文档

还有两个要记住的,就是mx-auto 和 my-auto

my-auto常用场景:在 flex 容器中垂直居中元素

/* my-auto 相当于 */
.my-auto {
  margin-top: auto;
  margin-bottom: auto;
}

mx-auto常用场景:水平居中块级元素

/* mx-auto 相当于 */
.mx-auto {
  margin-left: auto;
  margin-right: auto;
}

b.设置宽高:

这里只写高度,宽度同理

**h-**_**<number>**_ height: calc(var(--spacing) * _<number>_);
**h-full** height: 100%;
**h-screen** height: 100vh;
**h-dvh** height: 100dvh;
**h-dvw** height: 100dvw;
**h-[**_**<value>**_**]** height: _<value>_;

h-1:代表了height:0.25rem,也就是 height:4px

h-[5188px],代表了:height:5188px

高度继承可以使用:h-full来进行继承

c.文字颜色和背景颜色:

文字颜色跟背景颜色没什么好说的,虽然预设了很多值,但是能用的没几个,因为ui有自己的审美,所以一般都是用:text-[#xxxxxx] bg-[#xxxxxx]

<div className="mt-10 text-[#50d71e] bg-blue-400">测试tailwindcss</div>

文字加粗:

一般就是用font-normal 和 font-bold

**font-thin** font-weight: 100;
**font-normal** font-weight: 400;
**font-medium** font-weight: 500;
**font-bold** font-weight: 700;
**font-[**_**<value>**_**]** font-weight: _<value>_;

d.hover更改状态:

<button class="bg-sky-500 hover:bg-sky-700 ...">Save changes</button>

e.定位:

positon是我常忘记的,直接写属性值就行了

<div className="pointer-events-none fixed bottom-0 left-0">
  {process.env.DEPLOY_TIME}
</div>

f.鼠标样式:

一般就用到这两个

**cursor-pointer** cursor: pointer;
**cursor-not-allowed** cursor: not-allowed;

g.border样式:

圆角:我们在css经常写:border-radius:50%来画圆 ,这里可以用rounded-full来处理。

如果只是一些圆角,那么可以使用预设值或者自定义值来处理

**rounded-xs** border-radius: var(--radius-xs); _/* 0.125rem (2px) */_
**rounded-sm** border-radius: var(--radius-sm); _/* 0.25rem (4px) */_
**rounded-md** border-radius: var(--radius-md); _/* 0.375rem (6px) */_
**rounded-lg** border-radius: var(--radius-lg); _/* 0.5rem (8px) */_
**rounded-xl** border-radius: var(--radius-xl); _/* 0.75rem (12px) */_
**rounded-2xl** border-radius: var(--radius-2xl); _/* 1rem (16px) */_
**rounded-none** border-radius: 0;
**rounded-full** border-radius: calc(infinity * 1px);
**rounded-[**_**<value>**_**]** border-radius: _<value>_;

如果是需要画一条红色的虚线,则比原来的boder-bottom:1px dash red麻烦点,这里分成了三个属性,分别是border-width(负责宽度和哪个方向的边框),border-color(负责颜色)和border-style(负责虚线还是实线)。

下面是一个2px的虚线红色的下边框

<div className="mt-10 border-b-2 border-red-500 border-dashed">测试红色虚线</div>

h.单行(多行)文本超出显示...

传统css:

.my-ellipsis {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

在tailwind中有两种,可以使用简写的,也可以使用完整的三个属性

<div className="truncate">长文本...</div>
<div className="whitespace-nowrap overflow-hidden text-ellipsis">长文本...</div>

多行的传统css:

.text-ellipsis-multiline {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3; /* 显示3行 */
  overflow: hidden;
}

Tailwind CSS 提供了 line-clamp 工具类

<div className="line-clamp-3">
  多行文本内容...
</div>

常用的 line-clamp 类:

line-clamp-1 - 显示1行

line-clamp-2 - 显示2行

line-clamp-3 - 显示3行

line-clamp-4 - 显示4行

line-clamp-5 - 显示5行

line-clamp-6 - 显示6行

line-clamp-none - 取消行数限制

i:响应式布局

前缀 屏幕宽度 CSS 媒体查询
<font style="color:rgb(53, 148, 247);background-color:rgba(59, 170, 250, 0.1);">sm</font> ≥640px <font style="color:rgb(53, 148, 247);background-color:rgba(59, 170, 250, 0.1);">@media (min-width: 640px)</font>
<font style="color:rgb(53, 148, 247);background-color:rgba(59, 170, 250, 0.1);">md</font> ≥768px <font style="color:rgb(53, 148, 247);background-color:rgba(59, 170, 250, 0.1);">@media (min-width: 768px)</font>
<font style="color:rgb(53, 148, 247);background-color:rgba(59, 170, 250, 0.1);">lg</font> ≥1024px <font style="color:rgb(53, 148, 247);background-color:rgba(59, 170, 250, 0.1);">@media (min-width: 1024px)</font>
<font style="color:rgb(53, 148, 247);background-color:rgba(59, 170, 250, 0.1);">xl</font> ≥1280px <font style="color:rgb(53, 148, 247);background-color:rgba(59, 170, 250, 0.1);">@media (min-width: 1280px)</font>
<font style="color:rgb(53, 148, 247);background-color:rgba(59, 170, 250, 0.1);">2xl</font> ≥1536px <font style="color:rgb(53, 148, 247);background-color:rgba(59, 170, 250, 0.1);">@media (min-width: 1536px)</font>
<div class="text-sm md:text-base lg:text-lg">
  <!-- 移动端: 小号字, 平板: 基础字号, 桌面: 大号字 -->
</div>

j:最重要的布局类(flex和grid布局)

1.flex布局

flex这里不多讲,一般大家都是用的比较多,主要是看看在tailwind里面是怎么写的

display:flex.(原来css) => flex(在tailwindcss里面)

justify-content(主轴排列方式):

**justify-start** justify-content: flex-start;
**justify-end** justify-content: flex-end;
**justify-center** justify-content: center;
**justify-between** justify-content: space-between;
**justify-around** justify-content: space-around;
**justify-evenly** justify-content: space-evenly;
**justify-stretch** justify-content: stretch;

align-items(侧轴排列方式):

**items-start** align-items: flex-start;
**items-end** align-items: flex-end;
**items-center** align-items: center;
**items-baseline** align-items: baseline;
**items-stretch** align-items: stretch;

flex方向:

**flex-row** flex-direction: row;
**flex-row-reverse** flex-direction: row-reverse;
**flex-col** flex-direction: column;
**flex-col-reverse** flex-direction: column-reverse;

flex换行:

**flex-nowrap** flex-wrap: nowrap;
**flex-wrap** flex-wrap: wrap;
**flex-wrap-reverse** flex-wrap: wrap-reverse;

flex属性:flex 是 CSS Flexbox 中的简写属性,用于同时设置 flex-grow、flex-shrink 和 flex-basis,控制弹性项目在容器中如何伸缩以适应可用空间

这里面最常用的就是flex-1:允许弹性物品根据需要大小变化
意思就是:比如在一个flex盒子中,里面有3个子盒子,有两个子盒子设置了flex-1,还有一个没有设置,那么当宽度变化时,这个没设置的盒子的宽度是固定的,两个flex-1的盒子宽度会随着父盒子宽度的增加而增加,减小而减小。

gap:(用于设置flex和grid布局中子盒子的间距,这个gap用的很多)

**gap-**_**<number>**_ gap: calc(var(--spacing) * _<value>_);
**gap-[**_**<value>**_**]** gap: _<value>_;
**gap-x-**_**<number>**_ column-gap: calc(var(--spacing) * _<value>_);
**gap-x-[**_**<value>**_**]** column-gap: _<value>_;
**gap-y-**_**<number>**_ row-gap: calc(var(--spacing) * _<value>_);
**gap-y-[**_**<value>**_**]** row-gap: _<value>_;

如下图,主要就是用于子盒子之间的间距的,可以使用gap-来设置统一的间距,也可以gap-x-来设置x轴的间距

2.grid布局

gird真的很好用,能省写很多盒子嵌套,虽然使用flex都能解决布局问题,但是有时候使用grid布局会更加的优雅。

这里建议去学习一下阮一峰老师的grid的文档:ruanyifeng.com/blog/2019/0…

举两个例子说明grid的好用:
例子1:三列等宽布局

这里写一个三列等宽的grid布局: 这里的1fr相当于flex布局中子盒子的宽度设置为:flex:1的效果

display: grid;
grid-template-columns: repeat(3, 1fr);

在flex布局中有点麻烦,原因盒子肯定不止3个(所以子盒子不能使用flex:1,得使用百分比),超过3个就要使用flex-wrap: wrap;而且要考虑到盒子之间的间距:

.flex-container {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.flex-item {
  flex: 0 0 calc(33.333% - 12px); /* 减去 gap 的影响 */
}

例子2:项目实际例子

如图实现上面这个效果,如果要flex要用多少盒子嵌套,大家想一想应该有个基本的思维。

但是使用flex布局,只需要定义4列两行的布局。

<div className="grid grid-flow-col grid-cols-4 grid-rows-[repeat(2,auto)] gap-2 border-b py-4">
  <span className="text-s-body-m text-body-m">Customer Name</span>
  <span>{detailData?.clientName}</span>
  <span className="text-s-body-m text-body-m">Customer ID</span>
  <span>{detailData?.customerId}</span>
  <span className="text-s-body-m text-body-m">Bank CIF</span>
  <span>{detailData?.cif}</span>
  <span className="text-s-body-m text-body-m">Asset / Network</span>
  <span>
    {detailData?.assetCode}({detailData?.chainCode})
  </span>
</div>

可以看到,我这里基本上没有多余的盒子。

解释一下:

\

3.列一下原类和在taiwindcss里面的写法

grid-template-columns:属性定义每一列的列宽

**grid-cols-**_**<number>**_ grid-template-columns: repeat(_<number>_, minmax(0, 1fr));
**grid-cols-none** grid-template-columns: none;
**grid-cols-subgrid** grid-template-columns: subgrid;
**grid-cols-[**_**<value>**_**]** grid-template-columns: _<value>_;
**grid-cols-(**_**<custom-property>**_**)** grid-template-columns: var(_<custom-property>_);

grid-template-rows:属性定义每一行的行高。

**grid-rows-**_**<number>**_ grid-template-rows: repeat(_<number>_, minmax(0, 1fr));
**grid-rows-none** grid-template-rows: none;
**grid-rows-subgrid** grid-template-rows: subgrid;
**grid-rows-[**_**<value>**_**]** grid-template-rows: _<value>_;
**grid-rows-(**_**<custom-property>**_**)** grid-template-rows: var(_<custom-property>_);

grid-auto-flow:默认的放置顺序是"先行后列",即先填满第一行,再开始放入第二行(假设定义了3行3列,则从第一行从左到右排列过去)。 默认值为:row,如果设置的是:column,那么就是"先列后行"

**grid-flow-row** grid-auto-flow: row;
**grid-flow-col** grid-auto-flow: column;
**grid-flow-dense** grid-auto-flow: dense;
**grid-flow-row-dense** grid-auto-flow: row dense;
**grid-flow-col-dense** grid-auto-flow: column dense;

justify-items属性:设置单元格内容的水平位置(左中右),align-items属性:设置单元格内容的垂直位置(上中下)

**justify-items-start** justify-items: start;
**justify-items-end** justify-items: end;
**justify-items-center** justify-items: center;
**justify-items-stretch** justify-items: stretch;
**items-start** align-items: flex-start;
**items-end** align-items: flex-end;
**items-center** align-items: center;
**items-baseline** align-items: baseline;
**items-stretch** align-items: stretch;

justify-content属性:是整个内容区域在容器里面的水平位置(左中右),align-content属性:是整个内容区域的垂直位置(上中下)。

这个的属性跟flex上的属性基本上是一致的,就不列了,主要是要知道这个属性是干嘛的。

l.使用!来达到最高等级

在css中使用!important来强制该样式为最高等级,在tailwindcss中只需要在对应的css样式前面加一个!

<div className="bg-[#0094fc] h-20 mt-10 !bg-[#917d35]"></div>

背景色bg-[#917d35]会覆盖bg-[#0094fc]

m.复用类名

各种指令的效果请查看官方文档

只需要在index.css文件中使用@apply字段来应用多个样式

使用时这样子就行了:

效果:

注意:这里有个问题,就是虽然可以这样子复用类名,但是在输入的时候没有提示,相对来说没有特别友好。

n.自定义类名

tailwindcss. v4版本较v3版本有较大的升级,之前v3是在tailwind.config.js中自定义自己的样式。但是现在v4版本不需要了,只需要在统一的css文件里面进行定义即可。

现在使用@theme来自定义类名

@theme {
  /* 颜色扩展 - 这些会生成 text-brand-color, bg-brand-color 等类 */
  --color-brand-color: #c5d535;
  --color-bg-blue: #0094ff;
  /* 间距扩展 - 这些会生成  m-72, w-100 等类 */
  --spacing-100: 400px;
  /* 字体大小扩展 - 这些会生成 text-menu-choice, text-title-l 等类 */
  --font-size-title-l: 1.5rem;
  /* 阴影扩展 - 这些会生成 shadow-d-base, shadow-d-button 等类 */
  --shadow-d-button: 0 4px 4px 0 rgb(0 0 0 / 0.2);
  
}

使用:
这里定义了颜色,那么不管是文字还是背景则都可以使用这个自定义类名的颜色

<div className="bg-bg-blue mt-5">123</div>
<div className="bg-brand-color mt-5 text-brand-color">123</div>

使用@theme来定义的在输入时就有提示,相对来说就友好很多了。

以下是可以定义的内容:

文档:tailwindcss.com/docs/theme#…

Namespace Utility classes
**--color-*** Color utilities like **bg-red-500**
, **text-sky-300**
, and many more
**--font-*** Font family utilities like **font-sans**
**--text-*** Font size utilities like **text-xl**
**--font-weight-*** Font weight utilities like **font-bold**
**--tracking-*** Letter spacing utilities like **tracking-wide**
**--leading-*** Line height utilities like **leading-tight**
**--breakpoint-*** Responsive breakpoint variants like **sm:***
**--container-*** Container query variants like **@sm:***
and size utilities like **max-w-md**
**--spacing-*** Spacing and sizing utilities like **px-4**
, **max-h-16**
, and many more
**--radius-*** Border radius utilities like **rounded-sm**
**--shadow-*** Box shadow utilities like **shadow-md**
**--inset-shadow-*** Inset box shadow utilities like **inset-shadow-xs**
**--drop-shadow-*** Drop shadow filter utilities like **drop-shadow-md**
**--blur-*** Blur filter utilities like **blur-md**
**--perspective-*** Perspective utilities like **perspective-near**
**--aspect-*** Aspect ratio utilities like **aspect-video**
**--ease-*** Transition timing function utilities like **ease-out**
**--animate-*** Animation utilities like **animate-spin**

o.自定义主题

tailwindcss中主题切换的原理:这里的主题切换主要是通过theme中定义css变量,然后切换时就切换html上的类名来实现(官方文档

@import "tailwindcss";

@theme {
  /* 主题切换的一些变量 */
  --color-main-color: var(--main-color);
  --color-secondary-color: var(--secondary-color);
}
/* 主题切换 CSS 变量定义 */
:root {
  --main-color: rgba(232, 176, 176, 0.87);
  --secondary-color: rgba(51, 183, 159, 0.87);
}

.dark {
  --main-color: rgba(19, 18, 18, 0.87);
  --secondary-color: rgba(52, 5, 5, 0.87);
}
const toggleTheme = () => {
  document.documentElement.classList.toggle("dark");
};
<div>切换下面的主题盒子</div>
<Button type="primary" onClick={ ()=> toggleTheme() }>Button</Button>
<div className="bg-main-color w-100 h-100 text-secondary-color">主题相关的盒子</div>

五.tailwindcss局限性

官方文档:tailwindcss.com/docs/stylin…

有一个例子是:

<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-3 的 12px 全方向?
因为在 Tailwind 的样式表中,方向性的 padding 类(px-*, py-*)定义在通用 padding 类(p-*)之后,所以它们会覆盖 p-3 的效果。
简单说:最终是上下 4px,左右 8px 的 padding! 

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

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

Vite创建react项目

2026年2月25日 11:01

1.背景

为什么要是用vite来创建react项目,主要是cra已经不太适配react了,react官网也放弃了cra的创建,原因是react19和cra不太兼容。所以如果大家用的是18的话,还是可以继续用cra来创建的。参考官网
另外的话用vite默认创建的话就是不支持服务端渲染(SSR),没有seo(适合toB项目),如果项目要支持seo的话,需要配置,不如建议大家使用nextjs ,或者remix来创建项目(适合toC项目)

2.创建命令

 npm create vite@latest

下面跟着选就行了,输入项目名,选择react+ts(ts是否要看大家自己的选择,建议还是选上,ts是趋势。小项目或者时间紧的话可以直接使用js也可以。)

3.生成git仓库

由于这里生成的项目没有git仓库,所以我们git init来生成一个仓库。然后git add . ,git commit- m"" 来进行初次提交。

4.规划项目目录

在src文件夹下面建立这些文件夹,详细的作用对应如下:

5.删除多余的东西

main.tsx中一些删除掉的东西就没必要引入了

6.支持scss(其他的less等也可以,看自己熟悉哪个)

安装依赖

npm i sass -D

测试一下有没有用

错误使用方式(同名类名会导致样式覆盖):

正确使用方式(改为css module方式,这样子父子组件类名相同也不会样式覆盖了):

7.加入UI组件库,安装antDesign

a.安装依赖

npm i antd

b.测试(这样子你能在页面上看到蓝色的按钮就说明导入成功了)

import { Button } from 'antd';
function App() {
  return (
    <>
    <div className="ceshi">你好</div>
    <Button type="primary">Button</Button>
    </>
  )
}

export default App

c.兼容react19,请查看官方文档(因为vite初始化的react项目目前是19版本的了,所以需要兼容)

安装依赖

npm install @ant-design/v5-patch-for-react-19 --save

在main.tsx中导入

import '@ant-design/v5-patch-for-react-19';

8.vite配置@别名

配置别名分为两种配置,1.项目支持使用@符号代表src 2.让vscode编辑器支持输入@符号时有下一个目录的提示

1.项目支持使用@符号代表src

a.先安装ts中支持path的依赖

<font style="color:rgb(6, 6, 7);">@types/node</font>(用于 TypeScript 环境中的 <font style="color:rgb(6, 6, 7);">path</font> 模块)。如果没有安装,可以通过以下命令安装:

npm install --save-dev @types/node

b.配置vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path' // 导入 path 模块

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'), // 配置 '@' 别名指向项目根目录下的 src 文件夹
    },
  },
})

c.测试一下

2.让vscode编辑器支持输入@符号时有下一个目录的提示

可以看到上面截图有波浪线非常不舒服,并且输入@符号时没有下一个文件夹的提示
往这文件中(tsconfig.app.json)加入以下配置:

"compilerOptions": {
    "baseUrl": ".", // 设置基准路径
    "paths": {
      "@/*": ["src/*"] // 配置 '@' 别名指向 src 文件夹
    },
  }

说明
<font style="color:rgb(6, 6, 7);">tsconfig.json</font> 是项目的主配置文件,用于定义整个项目的 TypeScript 编译选项。:\ <font style="color:rgb(6, 6, 7);">tsconfig.app.json</font> 用于专门配置客户端应用代码。它扩展或覆盖了 <font style="color:rgb(6, 6, 7);">tsconfig.json</font> 中的设置,以满足应用代码的特定需求。例如,你可以在这里设置 <font style="color:rgb(6, 6, 7);">baseUrl</font><font style="color:rgb(6, 6, 7);">paths</font> 来定义路径别名\ <font style="color:rgb(6, 6, 7);">tsconfig.node.json</font> 用于配置 Node.js 环境中的 TypeScript,例如 Vite 的配置文件 <font style="color:rgb(6, 6, 7);">vite.config.ts</font>。它确保在 Node.js 环境中运行的代码(如构建脚本)使用正确的编译选项

成功的情况如下图(有路径提示了):

9.样式初始化(消灭浏览器差异以及盒子原有样式)

(1)normalize.css保证我们的代码在各浏览器的一致性

安装依赖

npm i normalize.css
main.ts引入
import 'normalize.css'
(2)消除盒子原有样式(简化版) 1.建立reset.scss,写入以下样式:
* {
    padding: 0;
    margin: 0;
    list-style: none;
    box-sizing: border-box;
  }
  html,
  body {
    height: 100%;
  }
  #root{
    height: 100%;
  }
2.在main.ts中引入:import ‘@/assets/css/reset.scss’
import '@/assets/css/reset.scss'

10.router-配置基础路由页面

安装依赖

npm install react-router-dom

在pages页面下建立这三个文件夹(Home,Layout,Login ,记得是index.tsx文件)并写入最简单的代码

function Layout(){
    return (
        <div>Layout页面</div>
    )
}
export default Layout

在router页面下建立index.tsx文件写入以下代码

import { createBrowserRouter } from 'react-router-dom'

import Home from '@/pages/Home'
import Layout from '@/pages/Layout'
import Login from '@/pages/Login'

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout></Layout>,
  },
  {
    path: '/home',
    element: <Home></Home>,
  },
  {
    path: '/login',
    element: <Login></Login>,
  },
])

export default router

在main.tsx的导入并使用(可以把app.tsx和index.scss删掉了,用不到了)

import { createRoot } from 'react-dom/client'
import 'normalize.css'
import '@/assets/css/reset.scss'
import '@ant-design/v5-patch-for-react-19'
import router from '@/router'
import { RouterProvider } from 'react-router-dom'

createRoot(document.getElementById('root')!).render(
  <RouterProvider router={router}></RouterProvider>
)

11.加入状态管理工具(这里以rtk+redux+react-redux为例)

a.安装依赖

npm install @reduxjs/toolkit react-redux

b.在store文件夹下面建立modules文件夹(用于区分仓库)和index.tsx(仓库的统一出口),文件夹如下

c.如上图,在modules下建立user.tsx,写入以下代码

import { createSlice } from '@reduxjs/toolkit'

export const userSlice = createSlice({
  name: 'user',
  initialState: {
    value: 0,
    userInfo: {},
  },
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    setUserInfo: (state, action) => {
      state.userInfo = action.payload
    },
  },
})

export const { increment, decrement, setUserInfo } = userSlice.actions

// 异步写法
const fetchUser = () => {
  return async (dispatch: any) => {
    const res = await Promise.resolve()  // 假设这里去发了后台接口获取用户信息
    dispatch(setUserInfo(res))
  }
}

export { fetchUser }

export default userSlice.reducer

d.在router中的index.tsx中写入以下代码:

// src/store.js
import { configureStore } from '@reduxjs/toolkit'
import userSlice from './modules/user'

const store = configureStore({
  reducer: {
    user: userSlice,
  },
})
export default store
// 推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

e.在主应用中使用 Redux Store

<font style="color:rgba(0, 0, 0, 0.9);">src/main.tsx</font> 文件d中,使用 <font style="color:rgba(0, 0, 0, 0.9);">Provider</font> 组件将 Redux Store 传递给 React 组件树

import { createRoot } from 'react-dom/client'
import 'normalize.css'
import '@/assets/css/reset.scss'
import '@ant-design/v5-patch-for-react-19'
import router from '@/router'
import { RouterProvider } from 'react-router-dom'
import { Provider } from 'react-redux'
import store from '@/store'

createRoot(document.getElementById('root')!).render(
  <Provider store={store}>
    <RouterProvider router={router}></RouterProvider>
  </Provider>
)

f.在login页面中测试使用(异步和同步在react中调用时写法是一样的)

import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, fetchUser } from '@/store/modules/user'
import { RootState, AppDispatch } from '@/store'

function Login() {
  // 使用 RootState 类型推断 state 的类型
  const count = useSelector((state: RootState) => state.user.value)
  // 使用 AppDispatch 类型推断 dispatch 的类型
  const dispatch = useDispatch<AppDispatch>()
  return (
    <div>
      Login页面
      {count}
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
      <button onClick={() => dispatch(fetchUser())}>获取用户数据</button>
    </div>
  )
}
export default Login

12.加入axios进行发送请求

a.建立request.ts

具体的封装可看:blog.csdn.net/weixin_4323…

import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import { message, Modal } from 'antd'
import store from '@/store'
import { getToken } from '@/utils/auth'

// 扩展 axios 的配置类型,添加自定义属性
declare module 'axios' {
  export interface AxiosRequestConfig {
    meta?: {
      responseAll?: boolean
    }
  }
}

// 定义后端返回的数据结构
interface ApiResponse<T = unknown> {
  code: number
  message: string
  data: T
}

// 创建 axios 实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // api 的 base_url,Vite 使用 import.meta.env
  timeout: 5000, // 请求超时时间
})

// request 拦截器
service.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    // 如果登录了,有 token,则请求携带 token
    const state = store.getState()
    const userInfo = state.user?.userInfo as { token?: string } | undefined
    const token = userInfo?.token || getToken()
    
    if (token) {
      // 让每个请求携带 token--['X-Token'] 为自定义 key 请根据实际情况自行修改
      config.headers['X-Token'] = token
    }
    return config
  },
  (error: AxiosError) => {
    // Do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

// response 拦截器
service.interceptors.response.use(
  /**
   * 下面的注释为通过 response 自定义 code 来标示请求状态,当 code 返回如下情况为权限有问题,登出并返回到登录页
   * 如通过 xmlhttprequest 状态码标识 逻辑可写在下面 error 中
   */
  (response: AxiosResponse<ApiResponse>) => {
    const res = response.data
    
    // 处理异常的情况
    if (res.code !== 200) {
      message.error({
        content: res.message || '请求失败',
        duration: 5,
      })
      
      // 403:非法的 token; 50012:其他客户端登录了; 401:Token 过期了;
      if (res.code === 403 || res.code === 50012 || res.code === 401) {
        Modal.confirm({
          title: '确定登出',
          content: '你已被登出,可以取消继续留在该页面,或者重新登录',
          okText: '重新登录',
          cancelText: '取消',
          onOk: () => {
            // 清除用户信息和 token
            // 这里需要根据你的实际 Redux action 来调整
            // store.dispatch(logout()) 
            localStorage.removeItem('token')
            location.reload() // 为了重新实例化 react-router 对象 避免 bug
          },
        })
      }
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      // 默认只返回 data,不返回状态码和 message
      // 通过 meta 中的 responseAll 配置来决定后台是否返回所有数据(包括状态码,message 和 data)
      const isBackAll = response.config.meta?.responseAll
      if (isBackAll) {
        return res as unknown as AxiosResponse
      } else {
        return res.data as unknown as AxiosResponse
      }
    }
  },
  (error: AxiosError) => {
    console.log('err' + error) // for debug
    message.error({
      content: error.message || '网络请求失败',
      duration: 5,
    })
    return Promise.reject(error)
  }
)

export default service


b.定义api文件user.ts:

import request from '@/utils/request'
import { SuccessResponse, PageParams, PageData } from './common-types'

// 定义用户相关的接口类型
export interface LoginParams {
  username: string
  password: string
}

export interface LoginResponse {
  token: string
  userInfo: {
    id: number
    username: string
    avatar?: string
  }
}

export interface UserInfo {
  id: number
  username: string
  email?: string
  avatar?: string
}

// ========== 场景1: 复杂返回数据,必须定义类型(推荐) ==========
/**
 * 用户登录
 * ✅ 返回数据复杂,定义类型可以获得完整的智能提示
 */
export function login(data: LoginParams) {
  return request<LoginResponse>({
    url: '/user/login',
    method: 'post',
    data,
  })
}

/**
 * 获取用户信息
 * ✅ 返回数据有固定结构,定义类型
 */
export function getUserInfo() {
  return request<UserInfo>({
    url: '/user/info',
    method: 'get',
  })
}

// ========== 场景2: 简单返回数据,可以内联类型 ==========
/**
 * 修改密码
 * ✅ 返回数据简单,可以直接内联类型,不用单独定义
 */
export function changePassword(oldPwd: string, newPwd: string) {
  return request<{ success: boolean }>({
    url: '/user/password',
    method: 'put',
    data: { oldPwd, newPwd },
  })
}

/**
 * 检查用户名是否存在
 * ✅ 返回单个布尔值,内联类型即可
 */
export function checkUsername(username: string) {
  return request<{ exists: boolean }>({
    url: '/user/check',
    method: 'get',
    params: { username },
  })
}

// ========== 场景3: 使用通用类型 ==========
/**
 * 删除用户
 * ✅ 使用通用的 SuccessResponse 类型
 */
export function deleteUser(id: number) {
  return request<SuccessResponse>({
    url: `/user/${id}`,
    method: 'delete',
  })
}

/**
 * 获取用户列表(分页)
 * ✅ 使用通用的 PageData 泛型类型
 */
export function getUserList(params: PageParams) {
  return request<PageData<UserInfo>>({
    url: '/user/list',
    method: 'get',
    params,
  })
}

// ========== 场景4: 不关心返回值,可以省略类型 ==========
/**
 * 用户登出
 * ⚠️ 不关心返回值,可以不定义类型(返回 unknown)
 */
export function logout() {
  return request({
    url: '/user/logout',
    method: 'post',
  })
}

/**
 * 上报用户行为日志
 * ⚠️ 不需要处理返回值,可以省略类型
 */
export function reportLog(action: string) {
  return request({
    url: '/log/report',
    method: 'post',
    data: { action, timestamp: Date.now() },
  })
}

// ========== 场景5: 返回完整响应(包括 code 和 message) ==========
/**
 * 获取用户信息(返回完整响应,包括 code 和 message)
 * 💡 当需要获取后端返回的状态码和提示信息时使用
 */
export function getUserInfoWithFullResponse() {
  return request<UserInfo>({
    url: '/user/info',
    method: 'get',
    meta: {
      responseAll: true, // 返回完整的响应数据
    },
  })
}


这是关于axios和ts使用的一些方案对比

最好的就是使用工具自动生成api的ts的类型,这样子就不用手动维护了。

现在我使用的是混合策略,关键是因为有时候后端没有swagger文档,或者很多项目不太规范

次优方案:混合策略

● 如果后端没有文档 → 使用这个

● 核心功能定义类型(登录、用户信息等)

● 次要功能简化处理(日志上报、简单操作等)

● 通用结构复用类型(分页、成功响应等)

13.加入代码检测工具lint-staged+husky

参考:blog.csdn.net/weixin_4323…

1.安装依赖包:

npm i lint-staged --save-dev

2.在 package.json 中配置 lint-staged,利用它来调用 eslint 和 stylelint 去检查暂存区内的代码

{
  // ...
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix"
    ]
  },
}

3.安装配置 husky

npm i husky --save-dev

4.在package.json中配置快捷命令,用来在安装项目依赖时生成 husky 相关文件

{
  // ...
  "scripts": {
    // ...
    "prepare": "husky && echo 'pnpm lint-staged' > .husky/pre-commit && chmod +x .husky/pre-commit"
  },
}

5.有时候配置的命令可能随着版本升级会变,但是在git commit时出现这个检测就说明弄成功了

14.template仓库

仓库地址:gitee.com/rui-rui-an/…

❌
❌