Nuxt UI v4 Collapsible 组件完全指南:基于 Reka UI 的折叠交互与状态控制

原创2026-10-07 20:15:28893 阅读
文章标签:前端UI组件

Nuxt UI v4 Collapsible 组件完全指南:基于 Reka UI 的折叠交互与状态控制

导读

Collapsible 是 Nuxt UI v4 中用于切换内容可见性的基础交互组件,基于 Reka UI 的 CollapsibleRoot、CollapsibleTrigger、CollapsibleContent 原子封装,并通过 Tailwind CSS 提供开合动画与主题定制能力。本文以 docs/content/docs/2.components/collapsible.md 为核心骨架,结合组件源码、主题定义、测试用例与 playground 示例,系统讲解 Collapsible 的插槽结构、unmount-on-hide、disabled、受控 open 状态等核心用法,并深入拆解其动画实现与可访问性设计,读完即可在项目中完整落地折叠面板场景。

Nuxt UI Collapsible 组件展开状态界面(浅色主题)

组件概览:一次折叠交互需要哪几个部分

Collapsible 的交互模型由三个角色构成:触发器(Trigger)、根容器(Root) 与 内容区(Content)。在 Nuxt UI 中,这一模型被收敛为一个组件与两个插槽:

  • 默认插槽(default)承载触发器,通常是 Button 或其他任意组件;
  • #content 插槽承载展开后显示的内容。

从 Collapsible.vue 的模板实现可以清晰看到这三者的组装关系:

<CollapsibleRoot v-slot="{ open }" v-bind="rootProps" data-slot="root" :class="ui.root({ class: [props.ui?.root, props.class] })">
  <CollapsibleTrigger v-if="!!slots.default" as-child>
    <slot :open="open" />
  </CollapsibleTrigger>

  <CollapsibleContent data-slot="content" :class="ui.content({ class: props.ui?.content })">
    <slot name="content" />
  </CollapsibleContent>
</CollapsibleRoot>

两点值得注意的细节:

  1. 默认插槽通过 as-child 直接透传给 CollapsibleTrigger,因此你放入的任意组件(按钮、链接等)天然具备触发器语义,无需手动绑定事件;
  2. 默认插槽向外暴露 { open } 作用域插槽属性,这意味着你可以根据当前开合状态动态调整触发器的外观(例如旋转图标、切换文案),下文“控制 open 状态”一节会具体展示。

基础用法:触发器 + 内容插槽

最基础的写法是在默认插槽中放置一个 Button,并在 #content 插槽中放置展开内容:

<template>
  <UCollapsible class="flex flex-col gap-2 w-48">
    <UButton
      label="Open"
      color="neutral"
      variant="subtle"
      trailing-icon="i-lucide-chevron-down"
      block
    />

    <template #content>
      <Placeholder class="h-48" />
    </template>
  </UCollapsible>
</template>

对应 MDC 语法:

:u-button{label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-down" block}

#content
:placeholder{class="h-48"}

这里的 block 让按钮占满容器宽度,trailing-icon 使用 Lucide 的 chevron-down 图标暗示“可展开”状态。在 playground 的 components/collapsible.vue 中,你可以看到一套更完整的变体:使用 icon="i-lucide-lightbulb" + trailing-icon 组合,并配合 variant="outline" 以及 :ui="{ trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200' }" 实现图标随状态旋转——这正是下面“With rotating icon”示例在真实页面中的落地形式。

关键 Prop 深度解析

unmount-on-hide:内容是否随折叠卸载

unmount-on-hide 控制折叠时内容区是否从 DOM 中卸载,默认值为 true(见 Collapsible.vue 中的 withDefaults 声明:unmountOnHide: true)。

  • 保持 true:折叠时内容从 DOM 移除,可减少页面节点数量,适合内容量大的场景;
  • 设置为 false:内容始终保留在 DOM 中,仅通过样式隐藏(高度归零 + overflow: hidden),折叠再展开时无需重新创建节点,适合需要保持表单输入值、滚动位置或动画上下文(如 <canvas>、视频)的场景。
<UCollapsible :unmount-on-hide="false" class="flex flex-col gap-2 w-48">
  <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-down" block />

  <template #content>
    <Placeholder class="h-48" />
  </template>
</UCollapsible>

官方文档特别提示:设置后你可以通过浏览器 DevTools 检查 DOM,观察折叠状态下内容是否仍然渲染。测试用例 test/components/Collapsible.spec.ts 中的 ['with unmountOnHide', { props: { ...props, unmountOnHide: false } }] 正是对这一分支的渲染验证。

disabled:整体禁用交互

设置 disabled 后,触发器不再响应点击,内容保持关闭:

<UCollapsible disabled class="flex flex-col gap-2 w-48">
  <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-down" block />

  <template #content>
    <Placeholder class="h-48" />
  </template>
</UCollapsible>

disabled 同样在 CollapsibleProps 的类型定义中从 Reka UI 的 CollapsibleRootProps 中 Pick 而来(见 Collapsible.vue 第 10 行的接口定义),并在测试中通过 ['with disabled', { props: { ...props, disabled: true } }] 覆盖渲染快照。

as:自定义渲染元素

as 允许把根容器渲染为其他元素或组件,默认值为 div。测试用例中 ['with as', { props: { ...props, as: 'section' } }] 验证了将根节点渲染为 <section> 的行为——当折叠面板作为文档语义区块时,这个 prop 可以配合无障碍树使用。

open / default-open:受控与非受控

  • default-open:非受控模式的初始状态,组件内部自行管理开合;
  • open + v-model:open:受控模式,由父组件完全掌控状态。

二者的底层实现在 Collapsible.vue 中通过 useForwardProps(reactivePick(props, 'as', 'defaultOpen', 'open', 'disabled', 'unmountOnHide'), emits) 透传给 Reka UI 的 CollapsibleRoot,因此状态管理、键盘交互与 ARIA 属性均由底层库保证。

进阶示例

控制 open 状态

官方文档提供了一个可交互示例 collapsible-open-example(引用自 collapsible.md 的 component-example 指令):通过 default-open 或 v-model:open 控制开合,并借助 define-shortcuts 组合式函数为折叠面板注册快捷键——按 O 键即可切换开合状态:

<script setup lang="ts">
const open = ref(false)

// 注册快捷键 O 切换状态
useShortcuts({ o: () => (open.value = !open.value) })
</script>

<template>
  <UCollapsible v-model:open="open" class="flex flex-col gap-2 w-48">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-down" block />

    <template #content>
      <Placeholder class="h-48" />
    </template>
  </UCollapsible>
</template>

受控模式最大的价值在于:触发器可以放在 Collapsible 之外,甚至完全移除——例如由导航菜单、工具栏按钮或页面级快捷键来驱动折叠面板,而触发器按钮只需要挂在组件内部的任意位置。这正是官方文档 ::tip 中强调的能力。

带旋转图标的触发器

利用默认插槽暴露的 { open } 作用域插槽属性,可以让按钮图标随状态旋转,直观反馈开合状态。以下代码改编自 playground 的 components/collapsible.vue:

<script setup lang="ts">
import { useAppConfig } from '#imports'

const appConfig = useAppConfig()
</script>

<template>
  <UCollapsible class="flex flex-col gap-2 w-48">
    <UButton
      class="group"
      icon="i-lucide-lightbulb"
      :trailing-icon="appConfig.ui.icons.chevronDown"
      color="neutral"
      variant="outline"
      label="Open"
      block
      :ui="{ trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200' }"
    />

    <template #content>
      <Placeholder class="h-48 w-full" />
    </template>
  </UCollapsible>
</template>

关键点在于 group 类 + group-data-[state=open]:rotate-180:data-state 是 Reka UI 写入根元素的属性(取值为 open / closed),配合 Tailwind 的 group-data-* 变体即可让触发器内部元素感知根状态。appConfig.ui.icons.chevronDown 保证了图标与全局主题配置一致。

主题定制与动画原理

默认主题插槽

Collapsible 的默认主题定义在 src/theme/collapsible.ts:

export default {
  slots: {
    root: '',
    content: 'data-[state=open]:animate-[collapsible-down_200ms_var(--ease-out)] data-[state=closed]:animate-[collapsible-up_200ms_var(--ease-out)] data-[state=closed]:overflow-hidden'
  }
}

可以看到根容器(root)默认无样式,内容区(content)默认绑定:

  • data-[state=open]:animate-[collapsible-down_200ms_var(--ease-out)]:展开时播放 collapsible-down 动画,时长 200ms,缓动函数为全局 CSS 变量 --ease-out;
  • data-[state=closed]:animate-[collapsible-up_200ms_var(--ease-out)]:收起时播放 collapsible-up 动画;
  • data-[state=closed]:overflow-hidden:关闭状态下隐藏溢出内容。

对应地,ui.root 与 ui.content 也支持通过 ui prop 或 appConfig.ui.collapsible 覆盖,测试中的 ['with ui', { props: { ...props, ui: { content: 'bg-elevated' } } }] 即验证了 ui.content 追加样式的行为。

动画 keyframes 的底层实现

collapsible-down / collapsible-up 两个关键帧动画定义在 src/runtime/keyframes.css 中,它们与 Reka UI 注入的 CSS 变量 --reka-collapsible-content-height 协同工作,实现精确的高度过渡:

@keyframes collapsible-up {
  from {
    height: var(--reka-collapsible-content-height);
    overflow: hidden;
  }
  to {
    height: 0;
    overflow: hidden;
  }
}

@keyframes collapsible-down {
  from {
    height: 0;
    overflow: hidden;
  }
  to {
    height: var(--reka-collapsible-content-height);
    overflow: hidden;
  }
}

高度动画方案与同一文件中的 accordion-up / accordion-down 一脉相承,原理是通过 Reka UI 测量的内容高度变量驱动 height 从 0 过渡到实际高度。此外,同文件底部的 @media (prefers-reduced-motion: reduce) 区块明确保留高度揭示类动画(注释原文:Height reveals (accordion, collapsible)... are intentionally left untouched),即折叠动画在“减少动态效果”系统设置下不会被禁用,保证内容可见性始终可感知。

这一动画组合并非 Collapsible 独有:从源码搜索可见,chat-reasoning.ts、chat-tool.ts、navigation-menu.ts 与 content-toc.ts 等组件的内容区同样复用了 collapsible-down / collapsible-up 动画,可以理解为 Nuxt UI 内部一套统一的高度展开/收起动画基础设施。

在 MDC 内容中使用 ProseCollapsible

除了组件 API,Nuxt UI 还在文档/内容渲染层提供了 ProseCollapsible(docs/content/docs/4.typography/collapsible.md),允许你在 Markdown 内容中用 ::collapsible 指令包裹任意内容:

::collapsible

| Prop    | Default   | Type                     |
|---------|-----------|--------------------------|
| `name`  |           | `string`                 |
| `size`  | `md`      | `string`                 |
| `color` | `neutral` | `string`                 |

::

ProseCollapsible 的实现位于 src/runtime/components/prose/Collapsible.vue,它直接内嵌了 UCollapsible(固定 :unmount-on-hide="false" 以保持内容在文档流中的连续性),并额外提供 icon、name、openText、closeText 等文案类 props——其中 icon 默认取 appConfig.ui.icons.chevronDown,name / openText / closeText 默认来自 locale 翻译(t('prose.collapsible.name') 等)。其默认主题 src/theme/prose/collapsible.ts 定义了 trigger、triggerIcon(含 group-data-[state=open]:rotate-180 旋转)、triggerLabel 与 content 插槽样式。对于面向文档、FAQ、API 参考等长内容折叠场景,这是比手写组件更快捷的路径。

API 速查与测试保障

Props / Slots / Emits 一览

  • Props:as(默认 div)、class、ui、defaultOpen、open、disabled、unmountOnHide(默认 true),完整类型见 Collapsible.vue 的 CollapsibleProps 接口;
  • Slots:default(props: { open: boolean }) 触发器插槽、content 内容插槽;
  • Emits:继承自 Reka UI 的 CollapsibleRootEmits(包含 update:open 等)。

可访问性与回归测试

test/components/Collapsible.spec.ts 对组件进行了多维度覆盖:

  1. renderEach 批量渲染快照:覆盖 open、as、unmountOnHide、disabled、class、ui 六类 props 与 default / content 两类插槽;
  2. 无障碍测试:通过 vitest-axe 的 axe(wrapper.element) 断言渲染结果无 ARIA 违规——由于底层 Reka UI 负责注入 aria-expanded、aria-controls、role 等语义属性,Collapsible 默认即可通过自动化无障碍检查。

这也印证了组件的可访问性能力来自底层封装而非额外手写,是“基于 Reka UI”设计理念的直接体现。

小结

Nuxt UI v4 的 Collapsible 用极薄的封装把 Reka UI 的展开/收起原语转换成了开箱即用的 Vue 组件:两个插槽解决结构组装,unmount-on-hide / disabled / open / default-open 解决常见状态需求,默认主题中的 collapsible-down / collapsible-up 动画与 --reka-collapsible-content-height 变量配合提供平滑的高度过渡,而 ProseCollapsible 则把同样的能力带进了 Markdown 内容。想要在页面中快速实现 FAQ、筛选面板或文档折叠块,从本文的基础示例出发即可,进阶的受控状态与图标旋转方案则覆盖了绝大多数真实业务形态。

相关文档与源码索引:

登录后查看全文
ui