这是一个CSS-only的解决方案,具有以下特点:
- 没有开始时的延迟,过渡也不会提前停止。在两个方向(扩展和折叠)中,如果在您的CSS中指定了300ms的过渡持续时间,则过渡需要花费300ms的时间。
- 它正在过渡实际高度(与
transform: scaleY(0)
不同),因此,如果在可折叠元素之后有内容,则会执行正确的操作。
- 虽然(与其他解决方案一样)存在魔数(例如“选择比您的框永远不会高的长度”),但如果您的假设最终是错误的,这并不致命。在此情况下,过渡可能看起来不太令人惊艳,但在过渡之前和之后,这不是问题:在扩展(
height: auto
)状态下,整个内容始终具有正确的高度(例如,如果您选择的max-height
太低)。在折叠状态下,高度为零,如预期。
演示
这里有一个演示,其中包含三个可折叠元素,所有元素高度都不同,但都使用相同的CSS。单击“运行片段”后,您可能需要单击“全屏”。请注意,JavaScript仅切换collapsed
CSS类,没有涉及任何测量。(您可以使用复选框或:target
在完全不使用JavaScript的情况下执行此示例)。此外,请注意负责过渡的CSS部分相当短,HTML仅需要一个额外的包装器元素。
$(function () {
$(".toggler").click(function () {
$(this).next().toggleClass("collapsed");
$(this).toggleClass("toggled");
});
});
.collapsible-wrapper {
display: flex;
overflow: hidden;
}
.collapsible-wrapper:after {
content: '';
height: 50px;
transition: height 0.3s linear, max-height 0s 0.3s linear;
max-height: 0px;
}
.collapsible {
transition: margin-bottom 0.3s cubic-bezier(0, 0, 0, 1);
margin-bottom: 0;
max-height: 1000000px;
}
.collapsible-wrapper.collapsed > .collapsible {
margin-bottom: -2000px;
transition: margin-bottom 0.3s cubic-bezier(1, 0, 1, 1),
visibility 0s 0.3s, max-height 0s 0.3s;
visibility: hidden;
max-height: 0;
}
.collapsible-wrapper.collapsed:after
{
height: 0;
transition: height 0.3s linear;
max-height: 50px;
}
#container {
display: flex;
align-items: flex-start;
max-width: 1000px;
margin: 0 auto;
}
.menu {
border: 1px solid #ccc;
box-shadow: 0 1px 3px rgba(0,0,0,0.5);
margin: 20px;
}
.menu-item {
display: block;
background: linear-gradient(to bottom, #fff 0%,#eee 100%);
margin: 0;
padding: 1em;
line-height: 1.3;
}
.collapsible .menu-item {
border-left: 2px solid #888;
border-right: 2px solid #888;
background: linear-gradient(to bottom, #eee 0%,#ddd 100%);
}
.menu-item.toggler {
background: linear-gradient(to bottom, #aaa 0%,#888 100%);
color: white;
cursor: pointer;
}
.menu-item.toggler:before {
content: '';
display: block;
border-left: 8px solid white;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
width: 0;
height: 0;
float: right;
transition: transform 0.3s ease-out;
}
.menu-item.toggler.toggled:before {
transform: rotate(90deg);
}
body { font-family: sans-serif; font-size: 14px; }
*, *:after {
box-sizing: border-box;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="container">
<div class="menu">
<div class="menu-item">Something involving a holodeck</div>
<div class="menu-item">Send an away team</div>
<div class="menu-item toggler">Advanced solutions</div>
<div class="collapsible-wrapper collapsed">
<div class="collapsible">
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
<div class="menu-item">Ask Worf</div>
<div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
<div class="menu-item">Ask Q for help</div>
</div>
</div>
<div class="menu-item">Sweet-talk the alien aggressor</div>
<div class="menu-item">Re-route power from auxiliary systems</div>
</div>
<div class="menu">
<div class="menu-item">Something involving a holodeck</div>
<div class="menu-item">Send an away team</div>
<div class="menu-item toggler">Advanced solutions</div>
<div class="collapsible-wrapper collapsed">
<div class="collapsible">
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
</div>
</div>
<div class="menu-item">Sweet-talk the alien aggressor</div>
<div class="menu-item">Re-route power from auxiliary systems</div>
</div>
<div class="menu">
<div class="menu-item">Something involving a holodeck</div>
<div class="menu-item">Send an away team</div>
<div class="menu-item toggler">Advanced solutions</div>
<div class="collapsible-wrapper collapsed">
<div class="collapsible">
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
<div class="menu-item">Ask Worf</div>
<div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
<div class="menu-item">Ask Q for help</div>
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
<div class="menu-item">Ask Worf</div>
<div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
<div class="menu-item">Ask Q for help</div>
</div>
</div>
<div class="menu-item">Sweet-talk the alien aggressor</div>
<div class="menu-item">Re-route power from auxiliary systems</div>
</div>
</div>
它是如何工作的?
实际上,这里涉及到两个过渡。其中一个将在扩展状态下的margin-bottom
从0像素转换为在折叠状态下的-2000px
(类似于这个答案)。这里的2000是第一个神奇数字,它基于您的框不会高于此(2000像素似乎是一个合理的选择)。
仅使用单独的margin-bottom
转换存在两个问题:
- 如果您有一个高度超过2000像素的框,则
margin-bottom:-2000px
不能隐藏所有内容,即使在折叠状态下也会有可见的内容。这只是一个小修复,我们稍后会处理。 - 如果实际框高度为1000像素,并且您的转换时间为300毫秒,则在约150毫秒后(或相反方向上晚150毫秒)可见转换已经结束。
解决这第二个问题的方法就是使用第二个过渡,该过渡概念上针对包装器的最小高度(“概念上”因为我们实际上并未使用min-height
属性进行此操作;稍后详细说明)。
这是一个动画,展示了如何结合底部边距过渡和最小高度过渡,两者持续时间相等,给我们提供了从全高到零高的组合过渡,持续时间相同。
左侧条显示了负底边距如何向上推动底部,从而减少可见高度。中间条显示了最小高度如何确保在折叠情况下,过渡不会提前结束,在扩展情况下,过渡不会延迟开始。右侧条显示了两者的组合如何导致框在正确的时间内从全高度过渡到零高度。
对于我的演示,我已经将50像素作为上限最小高度值。这是第二个神奇数字,它应该比框的高度更低。50像素也是合理的;您很少需要使元素可折叠,而它的高度甚至不到50像素。
从动画中可以看到,过渡效果是连续的,但不可导--当最小高度等于底部边距调整后的完整高度时,速度会突然变化。这在动画中非常明显,因为它对于两个转换都使用线性时间函数,并且整个过渡非常缓慢。在实际情况下(我在顶部的演示),过渡仅需要300毫秒,并且底部边距转换是不线性的。我尝试了许多不同的时间函数来处理这两个过渡,而我最终选择的那些感觉在最广泛的情况下效果最好。
还有两个问题需要解决:
- 上述的问题,即超过2000像素高度的框在折叠状态下不能完全隐藏,
- 以及相反的问题,在非隐藏状态下,少于50像素高度的框即使过渡不运行,最小高度也保持在50像素。
我们通过在折叠状态下给容器元素设置max-height: 0
并延迟0s 0.3s
来解决第一个问题。这意味着它不是真正的过渡,但是max-height
会有一个延迟应用,只有在过渡结束后才会应用。为了使其正确工作,我们还需要为相反状态选择一个数字max-height
。但与2000px情况不同,如果选择的数字太大,会影响过渡的质量,但在这种情况下,真的并不重要。因此,我们可以选择一个非常高的数字,以至于我们知道没有高度会接近它。我选择了一百万像素。如果您觉得可能需要支持超过一百万像素的内容,则1)很抱歉,并且2)只需添加几个零即可。
第二个问题是我们没有实际使用 min-height
进行最小高度过渡的原因。相反,在容器中有一个带有 height
的 ::after
伪元素,它从50px过渡到零。这具有与 min-height
相同的效果:它不会使容器缩小到低于伪元素当前高度的任何高度。但是因为我们使用的是 height
而不是 min-height
,所以我们现在可以使用 max-height
(再次延迟应用)将伪元素的实际高度设置为零,一旦过渡结束,确保即使是小的元素也具有正确的高度。因为 min-height
比 max-height
更强大,如果我们使用容器的 min-height
而不是伪元素的 height
,这种方法就行不通了。就像上一段中的 max-height
一样,这个 max-height
也需要相反端点的值。但在这种情况下,我们只需选择50px。
在 Chrome(Win、Mac、Android、iOS)、Firefox(Win、Mac、Android)、Edge、IE11(除了我没费力去调试的演示中的 flexbox 布局问题)和 Safari(Mac、iOS)中进行了测试。说到 flexbox,应该可以在不使用任何 flexbox 的情况下使其工作;实际上,我认为你可以让几乎所有东西在 IE7 中都工作——除了你没有 CSS 过渡,这使得它成为一个相当无意义的练习。