Nuxt UI v4 Collapsible 组件完全指南:基于 Reka 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 状态等核心用法,并深入拆解其动画实现与可访问性设计,读完即可在项目中完整落地折叠面板场景。
组件概览:一次折叠交互需要哪几个部分
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>
两点值得注意的细节:
- 默认插槽通过
as-child直接透传给CollapsibleTrigger,因此你放入的任意组件(按钮、链接等)天然具备触发器语义,无需手动绑定事件; - 默认插槽向外暴露
{ 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 对组件进行了多维度覆盖:
renderEach批量渲染快照:覆盖open、as、unmountOnHide、disabled、class、ui六类 props 与 default / content 两类插槽;- 无障碍测试:通过
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、筛选面板或文档折叠块,从本文的基础示例出发即可,进阶的受控状态与图标旋转方案则覆盖了绝大多数真实业务形态。
相关文档与源码索引:
- 官方文档:docs/content/docs/2.components/collapsible.md
- 组件实现:src/runtime/components/Collapsible.vue
- 默认主题:src/theme/collapsible.ts
- 动画关键帧:src/runtime/keyframes.css
- 内容版组件:src/runtime/components/prose/Collapsible.vue
- 测试用例:test/components/Collapsible.spec.ts
- 可运行示例:playgrounds/nuxt/app/pages/components/collapsible.vue
