惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
WordPress大学
WordPress大学
T
The Blog of Author Tim Ferriss
The GitHub Blog
The GitHub Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
A
About on SuperTechFans
Stack Overflow Blog
Stack Overflow Blog
雷峰网
雷峰网
Microsoft Azure Blog
Microsoft Azure Blog
腾讯CDC
爱范儿
爱范儿
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 【当耐特】
V
Visual Studio Blog
有赞技术团队
有赞技术团队
U
Unit 42
D
Docker
小众软件
小众软件
F
Full Disclosure
I
Intezer
Scott Helme
Scott Helme
P
Privacy International News Feed
P
Proofpoint News Feed
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
B
Blog
Martin Fowler
Martin Fowler
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Vercel News
Vercel News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Spread Privacy
Spread Privacy
宝玉的分享
宝玉的分享
S
Security Affairs
www.infosecurity-magazine.com
www.infosecurity-magazine.com
月光博客
月光博客
C
Cisco Blogs
云风的 BLOG
云风的 BLOG
Schneier on Security
Schneier on Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Threat Research - Cisco Blogs
量子位
Hacker News - Newest:
Hacker News - Newest: "LLM"
H
Heimdal Security Blog
N
Netflix TechBlog - Medium
H
Hacker News: Front Page
P
Proofpoint News Feed
G
GRAHAM CLULEY
V
Vulnerabilities – Threatpost
S
Schneier on Security

阿掖山·博客

.css | 动画笔记 | 阿掖山·博客 重写.bio | Gibson Assembly 技术组装 DNA 分子 | 阿掖山·博客 涌现(emergence) | Tiki Taka 和德国足协的双向奔赴 .bio | 分子生物学中的几种“子” | 阿掖山·博客 动力学[∞] | 从化学反应到基因调控 | 阿掖山·博客 .git | 基于工作流的 Git 学习笔记 | 阿掖山·博客 .bio | Gibson Assembly 技术组装 DNA | 阿掖山·博客 动力学[8] | 分岔 Bifurcation | 阿掖山·博客 动力学[7] | Criticality, 生物系统处于临界状态吗 | 阿掖山·博客 文革史笔记-2.4.3 | 罗瑞卿遭受身心的摧残 | 阿掖山·博客 .tex | 生物系统处于临界状态吗:最大熵原则 | 阿掖山·博客
ChatGPT | Radix Accordion 开合的动画效果 | 阿掖山·博客
阿掖山,一个博客 · 2026-07-15 · via 阿掖山·博客

《.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 更合适。