以《.css | 动画笔记》最后一个小节为 prompt,ChatGPT 的回答如下:

《.css | 动画笔记》最后一个小节为 prompt,ChatGPT 的回答如下:

对,这里确实有一点“Radix 魔法”,但它不是魔法,而是一个很经典的 headless component + CSS variable + runtime measurement 技巧。

var(--radix-accordion-content-height) 的本质是:

Radix 在 Content 展开/收起时,先测量真实内容高度,然后把这个高度写入一个 CSS custom property,CSS 动画只负责从 0 到这个变量值之间过渡。


为什么不能直接 animate height: auto?

这是关键。

CSS 动画不能直接从:

height: 0;

到:

height: auto;

因为 auto 不是一个具体数值。

例如:

.content {
  height: 0;
  transition: height .2s;
}

.content.open {
  height: auto;
}

浏览器不知道:

0px → ???px

应该怎么插值。

所以传统做法:

element.scrollHeight

拿到真实高度:

height: 237px;

然后动画:

0px → 237px

Radix 做了什么?

大概流程如下:

1. AccordionContent 渲染

你的:

<AccordionPrimitive.Content>

实际上是 Radix 的:

@radix-ui/react-accordion

里面的 Content。

它内部会有类似逻辑:

const height = contentRef.current.scrollHeight

content.style.setProperty(
  "--radix-accordion-content-height",
  `${height}px`
)

于是 DOM 变成:

<div
  data-state="open"
  style="
    --radix-accordion-content-height: 237px;
  "
>
  ...
</div>

2. Tailwind 动画读取变量

你的 CSS:

@keyframes accordion-down {
  from {
    height: 0;
  }

  to {
    height: var(--radix-accordion-content-height);
  }
}

实际运行:

height:
0px

237px

所以动画成立。


为什么每个 Accordion 实例高度不同?

因为 CSS variable 是写在 DOM 节点上的。

例如:

文章 A:

<div
 style="
 --radix-accordion-content-height:120px
 "
>
摘要 A
</div>

文章 B:

<div
 style="
 --radix-accordion-content-height:350px
 "
>
摘要 B
</div>

CSS:

height: var(--radix-accordion-content-height)

读取的是最近祖先作用域中的变量

CSS variable 是继承机制:

document
 |
 article A
 |
 content
     --height:120px

document
 |
 article B
 |
 content
     --height:350px

互不影响。


那为什么 HeadlessUI 没有?

因为 HeadlessUI 的设计哲学不同。

Radix:

我提供行为 + 状态 + 必要的测量,你负责样式。

HeadlessUI:

我提供状态,你负责动画。

例如 HeadlessUI Disclosure:

<Disclosure>
  {({open}) => (
    <>
      <DisclosureButton>
        Title
      </DisclosureButton>

      <DisclosurePanel>
        Content
      </DisclosurePanel>
    </>
  )}
</Disclosure>

它只告诉你:

open === true

但是:

  • 内容高度是多少?
  • 怎么测量?
  • height 动画怎么做?

它不管。


HeadlessUI 推荐怎么动画?

一般用:

<DisclosurePanel
  transition
  className="
    transition
    duration-200
    ease-out
    data-closed:-translate-y-2
    data-closed:opacity-0
  "
>

但是注意:

这里动画的是:

opacity
transform

不是:

height

因为 height auto 动画仍然是 CSS 难题。


如果你想在 HeadlessUI 复刻 Radix 效果

有几个方案。


方案 1:自己测量(最接近 Radix)

React:

const ref = useRef(null)

useEffect(()=>{
  if(ref.current){
    ref.current.style.setProperty(
      "--content-height",
      `${ref.current.scrollHeight}px`
    )
  }
},[])

然后:

.panel {
 overflow:hidden;
 animation:
   accordion-down .2s ease-out;
}

@keyframes accordion-down {
 from {
   height:0;
 }

 to {
   height:var(--content-height);
 }
}

这就是 Radix 的核心思想。


方案 2:使用 Framer Motion(React 世界更常见)

例如:

<motion.div
 initial={{height:0}}
 animate={{height:"auto"}}
 exit={{height:0}}
>
 content
</motion.div>

Framer Motion 内部帮你做:

measure scrollHeight
↓
animate numeric height
↓
restore auto

这是现在 React 项目非常常见的方法。


方案 3:CSS grid hack

现在也有一种纯 CSS 技巧:

.panel {
 display:grid;
 grid-template-rows:0fr;
 transition:grid-template-rows .2s;
}

.panel.open {
 grid-template-rows:1fr;
}

.inner {
 overflow:hidden;
}

React:

<div className="panel">
  <div className="inner">
    summary
  </div>
</div>

利用 grid 的:

0fr → 1fr

绕开:

height:auto

限制。

这个最近很流行。


所以总结一下 Radix 的“魔法”

实际上是三个技术组合:

Radix Accordion
        |
        |
        v
测量 DOM scrollHeight
        |
        |
        v
写入 CSS variable
--radix-accordion-content-height: xxxpx
        |
        |
        v
Tailwind animation
height:0 → var(...)

Radix 只是把这套复杂逻辑封装起来。

这个设计其实很符合它的定位:

  • Radix 管行为和无障碍
  • CSS 管视觉
  • CSS variable 是两者之间的桥梁

你之前从 shadcn/Radix 换到 HeadlessUI,丢失的其实不是 Accordion 功能,而是 Radix 提供的“动画基础设施”。如果你的博客文章卡片非常依赖这种“展开摘要”的体验,我会认为 Radix Accordion 或者 Framer Motion 会比 HeadlessUI Disclosure 更合适。

本文收录于以下合集: