我们都遇到过这种情况。 您有一个元素,希望能够使用 CSS 过渡效果使其平滑地折叠和展开,但其展开的大小需要根据内容确定。 您已设置了 transition: height 0.2s ease-out
。 您创建了一个 collapsed
CSS 类,它应用了 height: 0
。 您尝试了一下,结果……高度没有过渡。 它在两个尺寸之间切换,就好像从未设置过 transition
一样。 经过一番摸索,您发现此问题仅在高度最初或最终为 auto
时才会发生。 百分比、像素值、任何绝对单位都按预期工作。 但所有这些都需要预先硬编码特定高度,而不是允许它根据元素内容的大小自然确定。

在本文中,为了简单起见,我主要以 height
的形式进行描述,但此处的所有内容也适用于 width
。
如果您希望我有一个针对此问题的魔术般、完整的解决方案,我很抱歉让您失望了。 没有一种解决方案能够在没有缺点的情况下实现所需的效果。 但是,有多种变通方法,每种方法都有不同的优缺点,并且在大多数用例中,至少有一种方法能够以可接受的方式完成工作。 我将概述主要的方法,并列出它们的优缺点,以便您能够根据自己的情况选择最佳的方法。
为什么浏览器级别没有解决此问题?
根据 Mozilla 开发者网络文档,CSS 过渡规范有意排除了 auto 值。 似乎 一些人 已经提出了请求,但是如果您仔细考虑一下,它至少在一定程度上是有道理的。 基于内容及其与其他元素的交互方式重新计算所有元素的大小和位置的浏览器进程(称为“回流”)代价很高。 如果您要将元素过渡到 height
为 auto
,则浏览器必须对动画的每个阶段执行回流,以确定所有其他元素应该如何移动。 这无法以简单的方式进行缓存或计算,因为它在过渡发生之前不知道起始和/或结束值。 这将大大增加后台需要执行的计算量,并可能以开发人员可能没有意识到的方式降低性能。
max-height
方法 1:如果您在网上搜索此问题,max-height
方法可能会在最初的五到十个结果中被提及。 实际上,它并不是很理想,但我认为为了进行比较,在这里包含它是有价值的。
它的工作原理如下:CSS 值只能过渡到和来自固定单位值。 但假设我们有一个元素,其 height
设置为 auto
,但其 max-height
设置为固定值;例如,1000px
。 我们无法过渡 height
,但我们可以过渡 max-height
,因为它具有显式值。 在任何给定时刻,元素的实际高度将是 height
和 max-height
的最小值。 因此,只要 max-height
的值大于 auto
的结果,我们就可以简单地过渡 max-height
并实现所需效果的一个版本。
此方法有两个关键缺点
一个很明显,一个比较微妙。 明显的缺点是我们仍然必须为元素硬编码最大高度,即使我们不必硬编码高度本身。 根据您的情况,也许您可以保证您不需要超过那个高度。 但如果不是,这是一个很大的折衷。 第二个,不那么明显的缺点是,除非内容高度恰好与 max-height
相同,否则过渡长度实际上不会是您指定的长度。 例如,假设您的内容高 600px,并且您的 max-height
正在从 0px 过渡到 1000px,持续时间为 1 秒。 元素需要多长时间才能达到 600px? 0.6 秒!max-height
将继续过渡,但一旦达到其内容的末尾,真实高度将停止变化。 如果您的过渡使用非线性计时函数,情况会更加明显。 如果过渡在开始时很快,在结束时很慢,则您的部分将快速展开并缓慢折叠。 并不理想。 不过,过渡是相对主观的,因此在其他情况下此方法合适的情况下,它可能是一个可以接受的折衷方案。
transform: scaleY()
方法 2:如果您不熟悉 transform
属性,它允许您将 GPU 驱动的转换(平移、缩放、旋转等)应用于元素。 务必注意这些转换的一些特性。
- 它们将元素的可视表示视为图像,而不是 DOM 元素。 这意味着,例如,如果元素缩放得太远,它会看起来像素化,因为它的 DOM 最初渲染到比它现在跨越的像素更少的像素上。
- 它们**不会触发回流**。 同样,转换不知道也不关心元素的 DOM 结构,只关心浏览器绘制的“图片”。 这就是此方法有效的原因及其最大缺点。
实现方法如下:我们为元素的 transform
属性设置 transition
,然后在 transform: scaleY(1)
和 transform: scaleY(0)
之间切换。 它们分别表示“以元素开始时的相同比例(在 y 轴上)呈现此元素”和“以 0 的比例(在 y 轴上)呈现此元素”。 在这两种状态之间进行过渡将巧妙地将元素“压缩”到和从其自然的内容大小。 作为奖励,内部的字母和/或图像也会视觉上“压缩”自身,而不是滑出元素的边界。 缺点?由于没有触发回流,因此此元素周围的元素将完全不受影响。 它们既不会移动也不会调整大小以填充空隙。
此方法的优缺点非常明显
它要么非常适合您的用例,要么完全不适用。
这主要取决于文档流中是否有任何元素跟随所讨论的元素。 例如,像模态框或工具提示一样浮动在主文档上的内容将以这种方式完美工作。 对于文档底部的元素,它也可以工作。 但不幸的是,在许多情况下,这一个行不通。
方法 3:JavaScript
在 CSS 中管理 CSS 过渡效果将是理想的,但正如我们所了解的,有时它根本不可能实现。
如果您绝对需要具有平滑折叠部分,其展开大小完全由其内容驱动,并且页面上的其他元素将在它们过渡时围绕它们流动,则可以使用一些 JavaScript 来实现。
基本策略是手动执行浏览器拒绝执行的操作:计算元素内容的完整大小,然后将元素的 CSS 过渡到该显式像素大小。
让我们稍微分解一下。 首先要注意的是,我们使用 data-collapsed
属性跟踪部分当前是否已折叠。 这对于知道每次切换其展开时要对元素“做什么”是必要的。 如果这是一个 React 或 Angular 应用程序,这将是一个状态变量。
接下来可能会引起注意的是使用了requestAnimationFrame()
。它允许你在浏览器下次重新渲染时运行一个回调函数。在本例中,我们使用它来等待,直到我们刚刚设置的样式生效。这在我们更改元素的高度从auto
到等效的显式像素值时非常重要,因为我们不想在那里等待过渡。因此,我们必须清除transition
的值,然后设置height
,然后再恢复transition
。如果这些是在代码中的顺序行,结果就像它们都被同时设置了一样,因为浏览器不会并行于 JavaScript 执行进行重新渲染(至少,就我们的目的而言)。
另一个特性是在扩展完成发生后,我们将height
恢复为auto。我们使用transitionend
注册一个事件监听器,它在 CSS 过渡结束时触发。在该事件监听器内部,我们将其删除(因为我们只想让它响应紧随其后的过渡),然后从内联样式中删除height
。这样,元素的大小就恢复为页面正常样式定义的大小。我们不想假设它应该保持相同的像素大小,甚至不应该保持auto
大小。我们希望我们的 JavaScript 执行过渡,然后让路,并且不要过度干预。
其余部分相当简单。而且,正如你所看到的,这完全达到了预期的效果。也就是说,尽管尽了最大努力,但这种方法还是有很多方面会导致我们的代码变得脆弱且容易出错。
- 我们添加了 27 行代码,而不是 3 行。
- 对我们 section 元素中的
padding
或border-box
等内容的更改可能需要更改此代码。 - section 上的 CSS 过渡,如果恰好在高度过渡仍在进行时结束,可能会导致高度无法恢复到其默认值。
- 禁用一帧的
transition
可能会破坏该元素上同时发生的其它过渡。 - 如果某个错误导致元素的
height
样式与其data-collapsed
属性不同步,其行为可能会出现问题。
最重要的是,我们编写的代码是过程式的而不是声明式的,这本身就使其更容易出错且更复杂。尽管如此,有时我们的代码只需要做它需要做的事情,如果它值得权衡,那么它就值得权衡。
额外技巧:Flexbox
我称这种技巧为额外技巧,因为它在技术上并没有实现预期的行为。它提供了一种确定元素大小的替代方法,在许多情况下,它可能是合理的替代方案,并且完全支持过渡。
如果你还不熟悉 Flexbox,你可能想阅读关于Flexbox和flex-grow的文章。
Flexbox 是一个功能非常强大的系统,用于管理界面大小如何适应不同的情况。关于这一点已经写了很多文章,我不会详细介绍。我要介绍的是,鲜为人知的是,flex
属性以及与之相关的其它属性完全支持过渡!
这意味着,如果你的用例允许你使用 Flexbox 而不是内容大小来确定大小,那么让一个 section 平滑地折叠就变得非常简单,只需要设置transition: flex 0.3s ease-out
并切换flex: 0
即可。虽然不如基于内容的方式好,但比来回切换像素大小更加灵活(我知道,我知道,对不起)。
这里提供的解决方案不错,我以前经常遇到这种情况。
实际上,我一直使用一种混合了不透明度和字体大小的解决方案,如果内容只是文本,则很容易控制。
它只需要在不同时间更改字体大小和不透明度:当内容显示时,它首先将字体大小从 0 更改到所需大小,然后将其不透明度更改为 0;当你需要隐藏时,它首先变为透明,然后字体大小减小到 0,视觉效果更流畅 :)
这是我在之前项目中使用的示例代码:http://codepen.io/pedrorivera/pen/ZpAVwk
font-size
过渡会导致回流。感谢你全面总结了这个 CSS 过渡问题。我之前也使用过 max-height 的解决方法几次,但一直认为应该有更好的方法。至少现在我知道,虽然可能没有更好的方法,但还有其它选项可以尝试。
你可以通过使用
transitionEvent.propertyName
检查过渡类型并忽略非高度过渡来使transitioning
检查更健壮。另一种技巧是动画化
clip-path
。它还没有得到普遍支持,但 Jake Archibald 说 Chrome 正在努力对其进行硬件加速,因此它很快就会成为一个有吸引力的选择。很棒的文章!我刚开始深入学习 CSS,这真的很有帮助。
为了(可能)补充或提供上面“技巧 3:JavaScript”的替代方案,我一直使用以下方法作为我的菜单/垂直展开器过渡的首选方法。(具体来说,是只使用 jQuery 添加/删除类——尽管在这种情况下更多——而不是 jQuery 动画、slideToggle 等)。我欢迎大家对其中发现的任何问题/缺点提出意见。
CSS
(可以根据需要操作溢出状态以用于多级菜单等,当然,当没有 JS 时,可以将初始高度设置为 auto 或其它替代方案。)
jQuery
(在没有 JS 的情况下回退到即时打开/关闭。)
我尝试过许多这些“阅读更多”文本的解决方案,并且必须指定高度或最大高度,但不起作用,因为附加文本可以是几个句子到一个完整的页面。至少目前,我已经停止在折叠部分使用过渡。
最简单的解决方案可能是 Bootstrap 用于移动菜单的那个(如果我没记错的话)。至少我之前用过几次这个解决方案,效果很好。只需在每次切换之前计算切换元素的实际高度,并将其作为内联样式放入 max-height 中。其余部分相同——CSS 在 max-height 0 和内联指定的 max-height 之间进行过渡。
我之前使用过很多次。它确实需要在您第一次计算高度时项目可见。
我使用的是一个 js 解决方案:在折叠元素之前,我通过 element.scrollHeight 设置其高度,然后应用一个 css 类,其中包含 {height: 0px !important; overflow: hidden};
你有没有这个的 CodePen?我想试试。听起来很有趣。
我自己也使用过技巧 1,但没有硬编码 max-height,而是给它一个非常高的 max-height,并在过渡上使用三次贝塞尔曲线缓动,使其在悬停时从慢到快缓动,在取消悬停时从快到慢缓动。
http://codepen.io/foxareld/pen/XMgMvP
我认为在动画化手风琴时最好使用 javaScript(或 jQuery),因为其它仅使用 css 或混合 css 的 JS 选项无法完全实现目标。
jQuery 3.1.1 使用 requestAnimationFrame,因此动画化高度足够流畅。
这是最先进的技术……
仅供参考,CSS 工作组的 github 上有一个未解决的问题:https://github.com/w3c/csswg-drafts/issues/626。如果你想表达对 CSS 中原生解决方案的支持,请随时点赞。
哇,这个 flex 过渡技巧太棒了。从来没有想过这一点。太简单了 :)
很棒的文章和文档 Brandon。我们刚刚创建了一个包(react-css-collapse),使用了与你在技巧 3 中解释的非常相似的技术。也许这里有人会发现它有用 :)
我也对我的无障碍脚本进行了一些测试,最大高度的过渡效果不完美,但它确实起作用了(我测试了 scaleX 和 max-height 的组合,但目前还没有得到理想的结果)。如果在小屏幕上变得过于复杂(内容过多),您可以移除过渡效果,并简单地使用 display: none 。
另一个无障碍的关键点(如果您想避免可聚焦的隐藏内容):您无法对 display 进行动画。诀窍是,您可以对 visibility 进行动画并设置延迟(这样它将在过渡结束时“动画化”)。
Aaron Davidson 通过 JavaScript 获取高度测量值,然后以 CSS 自定义属性的形式将数据传递回 CSS 以进行过渡。
Maurice Carey 也偶然发现了 CSS 自定义属性的方法。
Jeroen Sormani 提供了这个示例。