出处:掘金

原作者:易师傅


在 Vue3 中使用 Headless UI

安装与使用

  1. 快速创建一个 Vue3 项目
pnpm create vite my-vue-app --template vue
  1. 安装 @headlessui/vue

因为市面上 Headless UI 无头组件库较多,为了方便大家上手,主要以 Tailwind Labs 团队开源的 headlessui 无头组件库为基本依赖

pnpm i @headlessui/vue
  1. 实现最基本样式组件

根据官网所示,一共提供了 10 个无头组件,咱们以其中具有代表性的 Listbox (Select) 为例

实现一个高度自定义符合 UI 设计师的 Select 组件

Select.vue

<template>
  <Listbox v-model="selectedRegion">
    <ListboxButton>{{ selectedRegion?.name || '请选择' }}</ListboxButton>
    <ListboxOptions>
      <ListboxOption
        v-for="item in regions"
        :key="item.id"
        :value="item"
        :disabled="item.unavailable"
      >
        {{ item.name }}
      </ListboxOption>
    </ListboxOptions>
  </Listbox>
</template>
 
<script setup>
  import { ref } from 'vue'
  import {
    Listbox,
    ListboxButton,
    ListboxOptions,
    ListboxOption,
  } from '@headlessui/vue'
 
  const regions = [
    { id: 1, name: '北京', unavailable: false },
    { id: 2, name: '上海', unavailable: false },
    { id: 3, name: '广州', unavailable: false },
    { id: 4, name: '深圳', unavailable: true },
    { id: 5, name: '香港', unavailable: false },
    { id: 5, name: '澳门', unavailable: false },
  ]
  const selectedRegion = ref()
</script>

代码其实很简单,渲染的样式的完全就是浏览器自带的,没有 UI,有的只是简单的交互逻辑

Tailwind css 实现

源码

  1. 安装 Tailwind 与初始化
pnpm install -D tailwindcss@latest postcss@latest autoprefixer@latest
 
npx tailwindcss init -p
  1. 添加 Tailwind 样式
<template>
  <Listbox v-model="selectedRegion">
    <ListboxButton class="w-[230px] h-[44px] text-[#999] outline-[#fff] flex items-center justify-between text-16 text-left bg-[#fff] px-[20px] rounded-[4px] border-[1px] border-solid border-[#e6e6e6]">
      {{ selectedRegion?.name || '请选择' }}
 
      <i class="block w-[16px] h-[16px] bg-[url(~/assets/pull.png)] bg-no-repeat bg-cover"></i>
    </ListboxButton>
    <ListboxOptions class="w-[230px] text-16 text-left bg-[#fff] rounded-[4px] border-[1px] shadow-[0px_3px_16px_2px_#e6e6e6]">
      <ListboxOption
        v-for="item in regions"
        :key="item.id"
        :value="item"
        :disabled="item.unavailable"
        as="template"
        v-slot="{ active, selected }"
      >
        <li
          :class="{
            'bg-[#f7f8fa] text-[#006aff]': active,
            'bg-white text-[#333333]': !active,
          }"
          class="h-[44px] leading-[44px] pl-[20px] cursor-pointer"
        >
          <CheckIcon v-show="selected" />
          
          {{ item.name }}
        </li>
      </ListboxOption>
    </ListboxOptions>
  </Listbox>
</template>
 
<script setup>
  import { ref } from 'vue'
  import {
    Listbox,
    ListboxButton,
    ListboxOptions,
    ListboxOption,
  } from '@headlessui/vue'
 
  const regions = [
    { id: 1, name: '北京', unavailable: false },
    { id: 2, name: '上海', unavailable: false },
    { id: 3, name: '广州', unavailable: false },
    { id: 4, name: '深圳', unavailable: false },
    { id: 5, name: '香港', unavailable: false },
    { id: 5, name: '澳门', unavailable: false },
  ]
  const selectedRegion = ref()
</script>

scss/less/css 使用

源码

<template>
  <Listbox v-model="selectedRegion">
    <ListboxButton class="box-button">
      {{ selectedRegion?.name || '请选择' }}
 
      <i class="box-button-icon"></i>
    </ListboxButton>
    <ListboxOptions class="list">
      <ListboxOption
        v-for="item in regions"
        :key="item.id"
        :value="item"
        :disabled="item.unavailable"
        as="template"
        v-slot="{ active, selected }"
      >
        <li
          :class="{
            'bg-[#f7f8fa] text-[#006aff]': active,
            'bg-white text-[#333333]': !active,
          }"
          class="list-item"
        >
          {{ item.name }}
        </li>
      </ListboxOption>
    </ListboxOptions>
  </Listbox>
</template>
 
<script setup>
  import { ref } from 'vue'
  import {
    Listbox,
    ListboxButton,
    ListboxOptions,
    ListboxOption,
  } from '@headlessui/vue'
 
  const regions = [
    { id: 1, name: '北京', unavailable: false },
    { id: 2, name: '上海', unavailable: false },
    { id: 3, name: '广州', unavailable: false },
    { id: 4, name: '深圳', unavailable: false },
    { id: 5, name: '香港', unavailable: false },
    { id: 5, name: '澳门', unavailable: false },
  ]
  const selectedRegion = ref()
</script>
 
<style scoped>
 
.box-button {
  width: 230px;
  height: 44px;
  color: #999;
  font-size: 16px;
  outline: #fff;
  display: flex;
  align-items: center;
  justify-content: space-between;
  text-align: center;
  background-color: #fff;
  padding: 0 20px;
  border-radius: 4px;
  border: 1px solid #e6e6e6;
}
 
.box-button-icon {
  display: block;
  width: 16px;
  height: 16px;
  background: url(~/assets/pull.png);
  background-repeat: no-repeat;
  background-size: cover;
}
 
.list {
  width: 230px;
  font-size: 16px;
  text-align: left;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #e6e6e6;
  box-shadow: 0 3px 16px 2px #e6e6e6;
}
 
.list-item {
  height: 44px;
  line-height: 44px;
  padding-left: 20px;
  cursor: pointer;
}
</style>

CSS in JS 实现

源码

在 Vue3 中,可以通过多种方式使用 CSS in JS。其中一种方法是使用 <style> 组件的特殊 v-bind 语法来动态绑定样式对象

<template>
     <ListboxButton :style="boxButton">
      {{ selectedRegion?.name || '请选择' }}
      <i :style="boxButtonIcon"></i>
    </ListboxButton>
    
    // do something
</template>
 
<script setup>
import { reactive } from 'vue';
 
const boxButton = reactive({
    width: '230px',
    height: '44px',
    color: '#999',
    fontSize: '16px',
    outline: '#fff',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    textAlign: 'center',
    backgroundColor: '#fff',
    padding: '0 20px',
    borderRadius: '4px',
    border: '1px solid #e6e6e6',
  
  })
 
  const boxButtonIcon = reactive({
    display: 'block',
    width: '16px',
    height: '16px',
    background: 'url(~/assets/pull.png)',
    backgroundRepeat: 'no-repeat',
    backgroundSize: 'cover',
  })
 
  const list = reactive({
    width: '230px',
    fontSize: '16px',
    textAlign: 'left',
    backgroundColor: '#fff',
    borderRadius: '4px',
    border: '1px solid #e6e6e6',
    boxShadow: '0 3px 16px 2px #e6e6e6',
  })
 
  const listItem = reactive({
    height: '44px',
    lineHeight: '44px',
    paddingLeft: '20px',
    cursor: 'pointer',
  })
</script>
 
<style scoped>
/* 这里可以编写其他的 CSS 规则 */
</style>

在 React 中使用 Headless UI

安装与使用

  1. 快速创建一个 React 项目
pnpm create vite my-react-app --template react-ts
  1. 安装 radix-ui

由于 React 的无头组件库甚多,且在 2023 年屌爆了一整年的 shadcn/ui 就是基于 radix-ui 无头组件库来实现,所以咱们以 radix-ui 作为基本依赖

以实现一个 tooltip 组件为例,来实现一个自定义样式的组件

因为 radix-ui 每个组件都要单独安装,所以咱们单独安装 @radix-ui/react-tooltip

其中的 @radix-ui/react-icons 是使用到 radix-ui 提供的 icon,大家可自行选择是否使用

pnpm install @radix-ui/react-tooltip @radix-ui/react-icons
  1. 实现最基本样式组件

Tooltip.tsx

import * as Tooltip from '@radix-ui/react-tooltip';
import { InfoCircledIcon } from '@radix-ui/react-icons';
 
const TooltipDemo = () => {
  return (
    <Tooltip.Provider>
      <Tooltip.Root>
        <Tooltip.Trigger asChild>
          <button>
            <InfoCircledIcon />
          </button>
        </Tooltip.Trigger>
        <Tooltip.Portal>
          <Tooltip.Content sideOffset={5}>
            解释说明文案
            <Tooltip.Arrow />
          </Tooltip.Content>
        </Tooltip.Portal>
      </Tooltip.Root>
    </Tooltip.Provider>
  );
};
 
export default TooltipDemo;

Tailwind css 实现

源码

import * as Tooltip from '@radix-ui/react-tooltip';
import { InfoCircledIcon } from '@radix-ui/react-icons';
 
const TooltipDemo = () => {
  return (
    <Tooltip.Provider>
      <Tooltip.Root delayDuration={0}>
        <Tooltip.Trigger asChild>
          <button className="text-violet11 shadow-blackA4 hover:bg-violet3 inline-flex h-[35px] w-[35px] items-center justify-center rounded-full bg-white outline-none hover:shadow-[0_2px_10px]">
            <InfoCircledIcon />
          </button>
        </Tooltip.Trigger>
        <Tooltip.Portal>
          <Tooltip.Content
            className="bg-[#000] text-white p-2 rounded-md text-xs"
            sideOffset={5}>
            这是一段鼠标悬浮后的解释说明文案
            <Tooltip.Arrow className="text-[#000]" />
          </Tooltip.Content>
        </Tooltip.Portal>
      </Tooltip.Root>
    </Tooltip.Provider>
  );
};
 
export default TooltipDemo;

scss/less/css 使用

源码

TooltipCss.css

.IconButton {
    border-radius: 50%;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 35px;
    width: 35px;
}
.IconButton:hover {
    box-shadow: 0 2px 10px #d9d9d9;
}
 
.TooltipContent {
    background-color: #000;
    color: #fff;
    padding: 2px 6px;
    border-radius: 4px;
    font-size: 13px;
}
 
.TooltipArrow {
    color: #000;
}

TooltipCss.tsx

const TooltipDemo = () => {
  return (
    <Tooltip.Provider>
      <Tooltip.Root delayDuration={0}>
        <Tooltip.Trigger asChild>
          <button className="IconButton">
            <InfoCircledIcon />
          </button>
        </Tooltip.Trigger>
        <Tooltip.Portal>
          <Tooltip.Content
            className="TooltipContent"
            sideOffset={5}>
            这是一段鼠标悬浮后的解释说明文案
            <Tooltip.Arrow className="TooltipArrow" />
          </Tooltip.Content>
        </Tooltip.Portal>
      </Tooltip.Root>
    </Tooltip.Provider>
  );
};

CSS in JS 使用

在 React 中使用 CSS in JS,一般有多种方式:

  • 内联样式(Inline Styles):直接在 JSX 元素上应用样式对象
  • 使用 styled-components 库:创建可以像组件一样使用的样式化组件
  • 使用 emotion 或 radium 库:这些库提供了类似 styled-components 的功能,同时也可以进行样式组合和优化
  • 使用 CSS 模块:将 CSS 提取为模块,可以避免 CSS 选择器冲突
  • 使用 @stitches/react 库

用比较常见的 Radium 库来进行举例

pnpm i radium
pnpm i -D radium @types/radium

具体代码:

import * as Tooltip from '@radix-ui/react-tooltip';
import { InfoCircledIcon } from '@radix-ui/react-icons';
import Radium from 'radium';
 
const TooltipDemo = () => {
  return (
    <Tooltip.Provider>
      <Tooltip.Root delayDuration={0}>
        <Tooltip.Trigger asChild>
          <button style={IconButtonStyles}>
            <InfoCircledIcon />
          </button>
        </Tooltip.Trigger>
        <Tooltip.Portal>
          <Tooltip.Content
            style={TooltipContentStyles}
            sideOffset={5}>
            这是一段鼠标悬浮后的解释说明文案
            <Tooltip.Arrow style={TooltipArrowStyles}/>
          </Tooltip.Content>
        </Tooltip.Portal>
      </Tooltip.Root>
    </Tooltip.Provider>
  );
};
 
const IconButtonStyles = {
  display: 'inline-flex',
  alignItems: 'center',
  justifyContent: 'center',
  width: 35,
  height: 35,
  borderRadius: '50%',
  outline: 'none',
  '&:hover': {
    boxShadow: '0 2px 10px #d9d9d9',
  },
}
 
const TooltipContentStyles = {
  backgroundColor: '#000',
  color: 'white',
  padding: '2px 6px',
  borderRadius: '4px',
  fontSize: '13px',
}
 
const TooltipArrowStyles = {
  color: '#000',
}
 
export default Radium(TooltipDemo);

比较 React 和 Vue 中 Headless UI 的异同

根据上述的实际使用,我们会发现其实无论是在 React 或 Vue 中,使用的 Headless UI 组件库,其实大同小异,都是要自定义样式、而且自定义样式的写法也几乎一致

可能最大的差一点,也就只有 React 和 Vue 编码方式语法糖的差异了,这个就得考验大家的基本功底

还有较大的差异点,就是第三方无头组件库的使用方式不同,这个主要取决于第三方组件库

其它 Headless UI 库

就目前市面上的,所有开源的无头组件库,几乎大部分都只支持 React,这个就不做解释了,懂的都懂

作者在这里就简要的收集了一些市面上的无头组件

适合 React

适合 Vue