✅ 使用 Vue+Zag+PandaCSS 实现一个超丝滑的勾选框组件

前言

ZagPandaCSS 都是出自 chakra 团队之手,Zag 聚焦于处理组件的逻辑,而 PandaCSS 聚焦于通过 ts 来维护样式,将两者进行搭配会有怎么样的使用体验呢?这篇文章将继续以 vuesax 中 checkbox 组件的样式作为参考,结合 Zag 和 PandaCSS 进行 vue3 版本的重构,实现一个超丝滑的勾选框组件。

如果你想学习有关 PandaCSS 和 Vue TSX 的前置知识,也可以参考以往的文章,前文回顾:

🚀一篇文章学会如何使用 JSX/TSX 开发 Vue 组件

🖱️使用 Vue+pandaCSS+tsx 实现一个超精致的按钮组件

😎使用 Vue+Zag+PandaCSS 实现一个超丝滑的对话框组件

🐼 PandaCSS 实现加载动画+ 🔧Vue3 自定义指令封装

使用 Zag 实现核心逻辑

Zag 中将 Checkbox 分为三个组成部分:

  • root:根元素,用于包裹内部元素
  • control: 控制器
  • label: 标签内容

我们首先在 Zag Checkbox 文档中复制 JSX 的实例代码:

jsx 复制代码
import * as checkbox from "@zag-js/checkbox"
import { normalizeProps, useMachine } from "@zag-js/vue"
import { defineComponent, h, Fragment, computed } from "vue"

export default defineComponent({
  name: "Checkbox",
  setup() {
    const [state, send] = useMachine(checkbox.machine({ id: "checkbox" }))

    const apiRef = computed(() =>
      checkbox.connect(state.value, send, normalizeProps),
    )

    return () => {
      const api = apiRef.value
      return (
        <label {...api.rootProps}>
          <span {...api.labelProps}>
            Input is {api.isChecked ? "checked" : "unchecked"}
          </span>
          <div {...api.controlProps} />
          <input {...api.hiddenInputProps} />
        </label>
      )
    }
  },
})

这段代码使用了 useMachine 函数创建了一个状态机,并且写了一个无样式的基础 checkbox 结构:

接下来我们为 checkbox 组件补充样式.

实现打勾动画图标

想要实现丝滑的勾选框效果,最直观的是打勾的动画。这个效果可以通过 SVG 或者纯 css 实现,这里我使用的是 css 来实现的。:

首先我们要想想如何实现一个勾勾的效果,✔️由一长一短两个斜边组成,那么我们只需要放置一个矩形,设置一定的旋转角度,并设置其中的两个边框,就能实现一个✔️的形状:

代码实现如下:

tsx 复制代码
import { defineComponent } from "vue";

import { css, cx } from "@/styled-system/css";

const IconCheck = defineComponent({
  props: {
    color: {
      type: String,
      default: css({ colorPalette: "gray" }),
    },
    size: {
      type: String,
      default: css({ colorPalette: "gray" }),
    },
    customCSS: {
      type: String,
    },
  },
  setup(props) {
    return () => (
      <i
        class={cx(
          css({
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
          }),
          props.customCSS,
        )}
      >
        <div
          class={css({
            position: "relative",
            width: "80%",
            height: "40%",
            transform: "rotate(-45deg)",
          })}
        >
          <div
            class={css({
              position: "absolute",
              left: "0",
              width: "full",
              height: "full",
              borderLeft: "2px solid white",
              animation: "checkShort 0.15s",
            })}
          />
          <div
            class={css({
              position: "absolute",
              left: "0",
              width: "full",
              height: "full",
              borderBottom: "2px solid white",
              animation: "checkLong 0.5s",
            })}
          />
        </div>
      </i>
    );
  },
});

export default IconCheck;

上面这段代码中,定义了一个矩形,宽高分别为最外层容器的 80%40%transform: "rotate(-45deg)", 则设置了矩形的旋转角度,内部放置两个与矩形宽高一致的容器,分别设置 borderLeftborderBottom ,这样就组成了一长一短两条线。

这里之所以需要在内部放两个容器单独设置边框,而不是直接在矩形设置边框,是为了更好的实现动画效果,长短边分别设置了两个持续时间不同的动画 checkShort 0.15scheckLong 0.5s

ts 复制代码
checkShort: {
  "0%": {
    height:0,
  },
  "100%":{
    height: "full",
  }
},
checkLong: {
  "0%": {
    width: 0,
  },
  "30%":{
    width: 0,
  },
  "100%": {
    width: "full",
  }
}

短边从最开始就执行动画,持续时间为长边动画的 30%,长边则在 0-30% 时不执行,30% 之后开始执行,这样就能实现短边动画执行完成后,长边动画再执行的效果。

之所以不使用 animation-delay 去延迟执行长边动画,是因为这种方式会导致动画延迟执行前,长边会先展示出来,效果就不对了。如果要使用这种方式还得单独做一些动画延迟前的隐藏处理,会比较麻烦:

实现 Hover 效果

为了让用户更容易感知勾选框是可以交互的,我们为勾选框增加未勾选和关状态的 hover 效果。

未勾选状态

未勾选状态的 hover 效果,默认只有灰色边框,鼠标悬浮后变为灰色背景:

这里有个注意点,我们鼠标悬浮在勾选框的最外层,也可以触发内层的 hover 样式,如果直接使用 hover 效果是没法做到的,这样只能鼠标悬浮在边框内才能触发。

如果我们没有使用任何样式库,实现这个效果可以通过维护一个 鼠标是否 hover 的状态,并通过 onMouseEnteronMouseLeave 来更新这个状态,再在内层根据这个状态动态渲染样式。

但这里我们可以使用 pandaCSSgroup 选择器来实现。

首先在勾选框最外层元素增加 group 类名:

diff 复制代码
<label
  {...api.rootProps}
  class={[
    css({
      display: "flex",
      alignItems: "center",
      cursor: "pointer",
    }),
+    "group",
  ]}
>

然后在内层的 control 元素增加基础样式:

tsx 复制代码
<div
    {...api.controlProps}
    class={css({
      width: "24px",
      height: "24px",
      borderRadius: "8px",
      border: api.isChecked
        ? "none"
        : "token(colors.gray.200) solid 2px",
      transition: "all 0.3s",
      marginRight: "4px",
      flexShrink: "0",
      _groupHover: {
        background: "gray.200",
      },
    })}
  >
  // ...
  </div>

这里的 _groupHover 即为 group 选择器,当带有 group 类名的元素触发 hover 时,内部的元素都可以使用 _groupHover 设置特定样式。这样我们就实现了前文图中的效果。

勾选状态

在勾选时,会有一个蓝色色块放大渐出的效果,我们先来实现这个样式。

tsx 复制代码
<Transition
  enterFromClass={css({
    transition: "all 0.2s",
    transform: "scale(0.5)",
    opacity: "0",
  })}
  enterToClass={css({
    transition: "all 0.2s",
    transform: "scale(1)",
    opacity: "1",
  })}
  leaveToClass={css({
    transition: "all 0.2s",
    transform: "scale(0.5)",
    opacity: "0",
  })}
>
  {api.isChecked && (
    <div
      class={cx(
        props.color,
        css({
          width: "full",
          height: "full",
          background: "colorPalette.600",
          borderRadius: "inherit",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          transition: "all 0.3s",
        }),
      )}
    >
      <IconCheck
        customCSS={css({
          width: "18px",
          height: "18px",
        })}
      />
    </div>
  )}
</Transition>

这里我们使用 vue 中的 Transition 组件来实现动画效果,通过改变scaleopacity 实现元素大小和透明度的过渡动画,内部包裹着勾选的图标。

实现了勾选的效果,继续实现勾选后的 hover 样式。勾选后在 hover 时,勾选框的外层有一个与主题色相同的外层阴影效果:

这里我们依然使用 group 选择器来设置 hover 样式:

diff 复制代码
<div
  class={cx(
    props.color,
    css({
      width: "full",
      height: "full",
      background: "colorPalette.600",
      borderRadius: "inherit",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      transition: "all 0.3s",
+      _groupHover: {
+        boxShadow:
+          "0px 3px 15px 0px color-mix(in srgb, token(colors.colorPalette.600) 40%, transparent)",
+      },
    }),
  )}
>
  <IconCheck
    customCSS={css({
      width: "18px",
      height: "18px",
    })}
  ></IconCheck>
</div>

在阴影效果的代码中 0px 3px 15px 0px color-mix(in srgb, token(colors.colorPalette.600) 40%, transparent) ,前面几个设置阴影大小的参数很容易理解,但是后面阴影颜色的实现大家可能比较陌生,单独解释一下:

  • token(colors.colorPalette.600)token 是 pandaCSS 中提供的一种 token 使用方法,用于在 borderboxSahdow 这种组合多个参数的样式中引用 token 值。相关文档:panda-css.com/docs/themin...

  • color-mix() : color-mix 则是原生 css 的函数,用于接收两个颜色值,并返回在指定色彩模式、以指定比例混合后的颜色。相关文档:developer.mozilla.org/zh-CN/docs/...

我这里用法的含义是在 srgb 的色彩模式下,将主题色(token(colors.colorPalette.600)) 与透明色(transparent),以 40% 的比例进行混合,最终合成的颜色就是 40% 透明度的主题色color-mix() 函数是 pandaCSS 中推荐用户用于为内置颜色设置透明度的方法,除此以外并没有发现其他更简便的方式。相关的讨论链接我也放在这里:github.com/chakra-ui/p...

完成双向绑定

最后我们完善一下勾选框的双向绑定逻辑逻辑,

  • 通过 Zag 中的 onCheckedChange 配合 emit 可以实现 Zag 内部状态向外传递
  • 通过监听 propmodelValue 的改变,执行 apiRef.value.setChecked 可以实现外部状态向 Zag 内传递

实现的代码如下:

ts 复制代码
const [state, send] = useMachine(
  checkbox.machine({
    id: useId("checkbox"),
    onCheckedChange(detail) {
      emit("update:modelValue", detail.checked);
    },
  }),
);

const apiRef = computed(() =>
  checkbox.connect(state.value, send, normalizeProps),
);

watch(
  () => props.modelValue,
  () => {
    apiRef.value.setChecked(props.modelValue);
  },
);

总结

本文对应的源码可以在这里看到 github.com/oil-oil/zax..., 后续的更新也都在这个仓库中, 快去点个 star 🌟

如果文章对你有帮助在收藏的同时也可以为我点个赞👍,respect!

相关推荐
excel3 分钟前
前端必备:从能力检测到 UA-CH,浏览器客户端检测的完整指南
前端
前端小巷子9 分钟前
Vue 3全面提速剖析
前端·vue.js·面试
悟空聊架构16 分钟前
我的网站被攻击了,被干掉了 120G 流量,还在持续攻击中...
java·前端·架构
CodeSheep17 分钟前
国内 IT 公司时薪排行榜。
前端·后端·程序员
尖椒土豆sss21 分钟前
踩坑vue项目中使用 iframe 嵌套子系统无法登录,不报错问题!
前端·vue.js
遗悲风22 分钟前
html二次作业
前端·html
江城开朗的豌豆26 分钟前
React输入框优化:如何精准获取用户输入完成后的最终值?
前端·javascript·全栈
CF14年老兵26 分钟前
从卡顿到飞驰:我是如何用WebAssembly引爆React性能的
前端·react.js·trae
画月的亮29 分钟前
前端处理导出PDF。Vue导出pdf
前端·vue.js·pdf
江城开朗的豌豆35 分钟前
拆解Redux:从零手写一个状态管理器,彻底搞懂它的魔法!
前端·javascript·react.js