☰
Ariakit Animated Disclosure:用纯 CSS 过渡动画实现 Disclosure 内容高度展开与折叠
2026/9/25 2:18:38 网站建设 项目流程
  • UI组件
  • 前端

【免费下载链接】ariakit

Toolkit with accessible components, styles, and examples for your next web app

项目地址:https://gitcode.com/gh_mirrors/ar/ariakit
点击查看免费下载

本篇指南围绕 Ariakit 仓库中的 Animated Disclosure 示例(examples/disclosure-animated)展开,演示如何在不依赖任何 JS 动画库的前提下,通过grid-template-rows过渡实现DisclosureContent高度从0到auto的展开/折叠动画。读完本文,你将掌握:Disclosure 内容的三层嵌套标记结构为何是必须的、[data-enter]选择器背后的状态机原理、以及aria-expanded驱动的图标旋转动画的完整落地方式。

示例总览与完整代码

该示例(readme)的目标非常明确:给 Disclosure 组件加上纯 CSS 过渡,让内容展开/折叠时高度平滑变化。它基于 WAI-ARIA Disclosure Pattern 构建,完整 API 构成如下(见 components/disclosure.md):

useDisclosureStore() useDisclosureContext() <DisclosureProvider> <Disclosure /> <DisclosureContent /> </DisclosureProvider>

示例的完整实现见 index.react.tsx,核心结构如下:

import * as Ariakit from "@ariakit/react"; import "./style.css"; export default function Example() { return ( <div className="wrapper"> <Ariakit.DisclosureProvider> <Ariakit.Disclosure className="button"> What are vegetables? {chevronIcon} </Ariakit.Disclosure> <Ariakit.DisclosureContent className="content-wrapper"> <div> <div className="content"> <p> Vegetables are parts of plants that are consumed by humans or other animals as food. ... </p> </div> </div> </Ariakit.DisclosureContent> </Ariakit.DisclosureProvider> </div> ); }

要点拆解:

  • <DisclosureProvider>创建一个 store,把Disclosure(触发按钮)与DisclosureContent(被控制的内容)关联起来;
  • <Disclosure>默认渲染为<button>,点击即调用 store 的toggle()切换open状态(disclosure.tsx);
  • <DisclosureContent>默认渲染为<div>,由 Ariakit 自动管理其hidden、data-open、data-enter、data-leave等属性。

内容标记结构:为什么需要两层中间 div

CSS 至今不支持对auto值做过渡(height: 0 → auto无法插值),因此示例采用了一个变通方案:用grid-template-rows的可插值特性间接动画高度。为此,内容树的组织方式必须遵循特定格式(摘自 readme):

<DisclosureContent className="content-wrapper"> <div> <div className="content"> ...

三层结构的职责划分:

层级元素职责
外层DisclosureContent(.content-wrapper)承担 grid 布局与grid-template-rows过渡
中间层匿名<div>grid 的直接子元素,必须overflow: hidden且不能有任何 padding,用于裁切折叠时的溢出内容
内层.content真正承载内容的 padding,因为 padding 放在中间层会导致0fr折叠时仍残留内边距高度

readme 特别强调:中间div是必需的("The intermediatedivis essential"),因为 grid 的直接子元素不能有 padding——把 padding 移到.content上才能做到真正折叠到 0 高度。

用 grid-template-rows 动画内容高度

在结构就位后,readme给出的核心 CSS(SCSS 嵌套写法)为:

.content-wrapper { display: grid; transition: grid-template-rows 150ms; grid-template-rows: 0fr; /* 等价于 height 从 0 过渡到 auto */ &[data-enter] { grid-template-rows: 1fr; } /* grid 的直接子元素必须隐藏溢出且无 padding */ > * { overflow: hidden; padding: 0; } }

原理:0fr与1fr是两个可插值的数值,grid-template-rows从0fr过渡到1fr时,grid 轨道的高度会随内容高度连续变化,视觉上完全等效于height: 0 → auto的过渡。

仓库中该示例的真实实现使用了 Tailwind 原子类(style.css),两者完全等价:

.content-wrapper { @apply grid grid-rows-[0fr] transition-[grid-template-rows] duration-200 >props = { "data-open": open || undefined, "data-enter": transition === "enter" || undefined, "data-leave": transition === "leave" || undefined, hidden, ...props, };

其背后的状态机值得理解:

  1. DisclosureContent渲染后,store 的animated状态被置为true(disclosure-content.tsx);
  2. 组件通过双重requestAnimationFrame(afterPaint)等待元素真正落屏后,再根据open/mounted判定把内部 transition 状态设为"enter"或"leave",从而挂载data-enter/data-leave属性(disclosure-content.tsx)。双重 rAF 是为了避免属性在元素完成渲染前被加上导致过渡不触发;
  3. 动画结束后,组件通过读取getComputedStyle中的transition-duration、animation-duration等计算最长结束时间(getElementEndTime),用超时器清除动画状态;如果检测到超时为 0(没有定义任何过渡),会直接把animated置回false,元素在关闭时立即卸载、跳过离场动画(disclosure-content.tsx);
  4. 内容不可见时(mounted === false等),组件会给元素加上display: none内联样式与hidden属性,保证动画结束后内容从可访问性树和布局中彻底移除(disclosure-content.tsx)。

这也解释了为什么样式只需写在[data-enter]一个选择器上:进入时 Ariakit 加上data-enter使grid-template-rows变为1fr;当open变为false后属性被移除,过渡自动反向回落到0fr完成折叠。

浏览器支持

grid-template-rows的可动画性已获所有现代浏览器支持。在旧浏览器中,内容会直接出现/消失而不播放动画——这是 readme 中明确说明的优雅降级行为,不会导致功能损坏。

旋转 Disclosure 箭头图标

展开/折叠的第二个视觉反馈是按钮上箭头图标的旋转。示例使用 CSS 的rotate属性配合aria-expanded选择器实现(摘自 readme):

.button > svg { transition: rotate 150ms; [aria-expanded="true"] > & { rotate: 180deg; } }

对应的 Tailwind 写法见 style.css:

.button { @apply ... [&>svg]:transition-[rotate] [&>svg]:aria-expanded:[rotate:180deg] }

aria-expanded属性由 Ariakit 自动维护:从 disclosure.tsx 可以看到,Disclosure会合并出"aria-expanded": expanded与"aria-controls": contentElement?.id,其中expanded由 store 的open状态驱动。因此图标动画不需要任何 JS 状态——只要open变化,aria-expanded变化,CSS 过渡即自动触发。这正是 WAI-ARIA Disclosure Pattern 要求触发器携带aria-expanded的可访问性收益在视觉层的一次复用(样式指南另见 guide/200-styling/readme.md)。

小结与相关示例

该方案的价值在于:动画完全由 CSS 声明式驱动,Ariakit 只负责在正确的时机挂载data-enter/data-leave与aria-expanded等语义属性,且内置了动画时长探测与降级逻辑——没有动画的浏览器环境下内容直接显隐,功能不受影响。

同一套思路在仓库中还有多组变体可以参考:

  • 动画 Dialog:examples/dialog-animated/index.react.tsx
  • 动画 Select:examples/select-animated/index.react.tsx
  • 动画 Combobox:examples/combobox-animated/index.react.tsx
  • 动画 Tab Panel:examples/tab-panel-animated/index.react.tsx
  • 基于 Framer Motion 的 Menu:examples/menu-framer-motion/index.react.tsx
  • 基于 Framer Motion 的 Tooltip:examples/tooltip-framer-motion/index.react.tsx

需要说明的适用前提:本示例代码文件标注为 Ariakit Plus 许可(见 index.react.tsx 头部注释),但其中演示的grid-template-rows+data-enter技术方案与组件本身的用法,同样适用于@ariakit/react的Disclosure/DisclosureContent组件。

  • UI组件
  • 前端

【免费下载链接】ariakit

Toolkit with accessible components, styles, and examples for your next web app

项目地址:https://gitcode.com/gh_mirrors/ar/ariakit
点击查看免费下载
上一篇:OpenSwiftUI:跨平台的SwiftUI开源实现
下一篇:终极指南:如何用HunterPie v2提升你的怪物猎人游戏体验

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询