Tailwind CSS + cva 实现样式变体组件

前言

button 组件是我们在开发中经常会用到的组件,这是一个看起来十分简单的组件,但是在实际开发中,我们经常会遇到这样的需求:同一个 button 组件,需要实现不同的样式,比如:不同的颜色、不同的大小、不同的形状等等。那么,我们如何通过 Tailwind CSS 优雅的实现这样的需求呢?

需求

为了方便演示,我们先来简单定义一下我们的基本需求:

  • button 组件有几种语义类型 type:primary、secondary、success、danger 等
  • button 组件有三种大小 size:small、medium、large
  • button 组件有三种填充 fill:solid、outline、text
  • button 组件有两种形状 shape:方形(小圆角)、胶囊形(full rounded)
  • button 组件支持 disabled
  • button 组件支持 Icon
  • button 组件支持 loading

简单分析一下需求,前四个需求都是对应按钮的不同样式变体(variants),我们只需要给每种变体指定自己特有的样式即可。而 disabled 和 loading 对应按钮的不同状态。我们使用组合的方式构建 Button 组件,因此 Icon 的逻辑不必写在 Button 组件内部。这样的话,loading 态也可以分解成一个 loading-icon + disabled 的组合。

初步实现

我们会用到一个 clsx 库,它可以帮助我们更方便的通过条件去控制样式的变化。我们先来看一下我们的 Button 组件的代码:

jsx 复制代码
import clsx from "clsx";

interface Props
  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "type"> {
  type?: "primary" | "success" | "danger";
  size?: "small" | "large" | "middle";
  fill?: "solid" | "outline" | "text";
  shape?: "round" | "square";
  disabled?: boolean;
}
export const Button = ({
  type = "primary",
  size = "middle",
  fill = "solid",
  shape = "square",
  children,
  ...props
}: Props) => {
  return (
    <button
      className={clsx(
        "inline-flex items-center justify-center rounded-lg text-sm font-medium disabled:opacity-20",
        {
          "bg-primary text-primary border-primary": type === "primary",
          "bg-success text-success border-success": type === "success",
          "bg-danger text-danger border-danger": type === "danger",
          "h-8  px-2 py-2": size === "small",
          "h-10 px-4 py-2": size === "middle",
          "h-12 px-6 py-2": size === "large",
          "text-white": fill === "solid",
          "border bg-white": fill === "outline",
          "bg-white": fill === "text",
          "!rounded-full": shape === "round",
        }
      )}
      {...props}
    >
      {children}
    </button>
  );
};

以上代码基本实现了我们的需求,但是存在两个问题:

  1. clsx 中大量的条件判断代码,使代码看起来很臃肿,没有组织性,不利于代码的阅读与维护。
  2. 我们无法保证 tailwind 中 class 的优先级,我们无法保证写在后面的样式可以覆盖前面的样式,比如 shape 为 round 的情况下,我们希望rounded-full更够覆盖基本样式中rounded-lg,但是我们无法保证这一点。只能通过!rounded-full来覆盖rounded-lg!important的使用会导致样式的不可预测性,不利于代码的维护。

使用 cva + tailwind-merge 改写

cva 是 Class Variance Authority 的缩写,cva 是一个用于管理样式变体的库,它可以帮助我们更好的组织样式变体,使得代码更加清晰,更加易于维护。我们可以使用 cva 来重写我们的 Button 组件,使得代码更加清晰。具体的用法,可以去看官方文档。这里就不做过多用法介绍了。

tailwind-merge 用来处理 tailwind 样式冲突问题,它可以让写在后面的样式覆盖前面的样式,这样我们就不需要使用!important来覆盖样式了。

以下是重构后的代码:

jsx 复制代码
import { cva, type VariantProps } from "class-variance-authority";
import clsx from "clsx";
import { twMerge } from "tailwind-merge";

interface Props
  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "type">,
    VariantProps<typeof buttonVariants> {}

const buttonVariants = cva(
  "inline-flex rounded-lg items-center justify-center text-sm font-medium disabled:opacity-50",
  {
    variants: {
      type: {
        primary: "bg-primary text-primary border-primary",
        success: "bg-success text-success border-success",
        danger: "bg-danger text-danger border-danger",
      },
      size: {
        middle: "h-10 px-4 py-2",
        small: "h-8  px-2 py-2",
        large: "h-12 px-6 py-2",
      },
      fill: {
        solid: "text-white",
        outline: "border bg-white",
        text: "bg-white",
      },
      shape: {
        round: "rounded-full",
        square: "rounded-lg",
      },
    },
    defaultVariants: {
      type: "primary",
      size: "middle",
      fill: "solid",
      shape: "square",
    },
  }
);

export const Button = ({
  type,
  size,
  fill,
  shape,
  children,
  ...props
}: Props) => {
  return (
    <button
      className={twMerge(clsx(buttonVariants({ type, size, fill, shape })))}
      {...props}
    >
      {children}
    </button>
  );
};

可以看到,改写后的 Button 组件清晰简洁了很多,我们把样式变体的定义和组件的实现分离开来,使得代码更加清晰,更加易于维护。

使用 twMerge 来处理样式冲突问题,使得我们可以更加方便的控制样式的优先级。

一些优化点

  1. 如果组件的使用场景是 PC 端,可以添加focus-visible伪类,美化键盘聚焦 Button 时的样式。还可以给不同的 type 加上对应的 hover 态样式。
  2. 如果组件的使用场景是移动端,可以添加active伪类,美化点击 Button 时的样式。
  3. 如果希望外部对 Button 组件的样式进行覆盖,可以使用className属性,这样可以让 Button 组件更加灵活。代码如下:
jsx 复制代码
export const Button = ({
  className,
  type,
  size,
  fill,
  shape,
  children,
  ...props
}: Props) => {
  return (
    <button
      className={twMerge(
        clsx(buttonVariants({ type, size, fill, shape, className }))
      )}
      {...props}
    >
      {children}
    </button>
  );
};

总结

本文通过一个简单的 Button 组件,介绍了如何使用 cva 和 tailwind-merge 来更好的组织样式变体,使得代码更加清晰,更加易于维护。

欢迎在评论区留下你的看法,如果觉得本文对你有帮助,欢迎点赞、收藏。十分感谢!!!

相关推荐
cy玩具14 分钟前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161771 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test1 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo2 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v2 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫2 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.2 小时前
Chrome调试工具(查看CSS属性)
前端·chrome