We’ve all been there. You’ve got an element you want to be able to collapse and expand smoothly using CSS transitions, but its expanded size needs to be content-dependent. You’ve set transition: height 0.2s ease-out
. You’ve created a collapsed
CSS class that applies height: 0
. You try it out, and… the height doesn’t transition. It snaps between the two sizes as if transition
had never been set. After some fiddling, you figure out that this problem only happens when the height starts out or ends up as auto
. Percentages, pixel values, any absolute units work as expected. But all of those require hard coding a specific height beforehand, rather than allowing it to naturally result from the size of the element content.
In this article, I mostly speak in terms of height
for simplicity, but everything here also applies to width
.
If you were hoping I had a magical, complete solution to this problem, I’m sorry to disappoint you. There’s no one solution that achieves the desired effect without downsides. There are, however, multiple workarounds that each come with a different set of advantages and disadvantages, and in most use cases at least one of them will get the job done in an acceptable manner. I’ll outline the major ones, and list out their ups and downs so you can hopefully pick the best one for your situation.
Why hasn’t this problem been fixed at the browser level?
According to the Mozilla Developer Network docs, auto values have been intentionally excluded from the CSS transitions spec. It looks like it’s been requested by a few people, but when you think about it, it makes at least a little sense that it hasn’t been included. The browser process that re-calculates the sizes and positions of all elements based on their content and the way they interact with each other (known as “reflow”) is expensive. If you were to transition an element into a height
of auto
, the browser would have to perform a reflow for every stage of that animation, to determine how all the other elements should move. This couldn’t be cached or calculated in a simple way, since it doesn’t know the starting and/or ending values until the moment the transition happens. This would significantly complicate the math that has to be done under the hood and probably degrade performance in a way that might not be obvious to the developer.
max-height
Technique 1: If you web search this problem, the max-height
approach will probably be mentioned in all of the first five to ten results. It’s actually pretty unideal, but I thought it was worth including here for the sake of comparison.
It works like this: CSS values can only be transitioned to and from fixed unit values. But imagine we have an element whose height
is set to auto
, but whose max-height
is set to a fixed value; say, 1000px
. We can’t transition height
, but we can transition max-height
, since it has an explicit value. At any given moment, the actual height of the element will be the minimum of the height
and the max-height
. So as long as max-height
‘s value is greater than what auto
comes out to, we can just transition max-height
and achieve a version of the desired effect.
There are two crucial downsides to this
One is obvious, and one is subtle. The obvious disadvantage is that we still have to hard-code a maximum height for the element, even if we don’t have to hard-code the height itself. Depending on your situation, maybe you can guarantee that you won’t need more height than that. But if not, it’s a pretty big compromise. The second, less obvious downside, is that the transition length will not actually be what you specify unless the content height works out to be exactly the same as max-height
. For example, say your content is 600px tall, and your max-height
is transitioning from 0px to 1000px with a duration of 1 second. How long will it take the element to get to 600px? 0.6 seconds! The max-height
will continue transitioning, but the real height will stop changing once it reaches the end of its content. This will be even more pronounced if your transition is using a nonlinear timing function. If the transition is fast at the beginning and slow at the end, your section will expand quickly and collapse slowly. Not ideal. Still, transitions are relatively subjective, so in cases where this technique is otherwise appropriate, it could be an acceptable tradeoff.
transform: scaleY()
Technique 2: If you aren’t familiar with the transform
property, it allows you to apply GPU-driven transformations (translate, scale, rotate, etc.) to an element. It’s important to note a couple of things about the nature of these transformations:
- They operate on the element’s visual representation as if it were simply an image, rather than a DOM element. This means, for example, that an element scaled up too far will look pixellated, since its DOM was originally rendered onto fewer pixels than it now spans.
- They do not trigger reflows. Again, the transform doesn’t know or care about the element’s DOM structure, only about the “picture” the browser drew of it. This is both the reason this technique works and its biggest downside.
Implementation works like this: we set a transition
for the element’s transform
property, then toggle between transform: scaleY(1)
and transform: scaleY(0)
. These mean, respectively, “render this element at the same scale (on the y axis) that it starts out at” and “render this element at a scale of 0 (on the y axis)”. Transitioning between these two states will neatly “squish” the element to and from its natural, content-based size. As a bonus, even the letters and/or images inside will visually “squish” themselves, rather than sliding behind the element’s boundary. The downside? Since no reflow is triggered, the elements around this element will be completely unaffected. They will neither move nor resize to fill in the empty space.
The advantages and disadvantages of this approach are stark
It will either work very well for your use-case or won’t be appropriate at all.
This mainly depends on whether or not any elements follow the one in question in the flow of the document. For example, something that floats over the main document like a modal or a tooltip will work perfectly this way. It would also work for an element that’s at the bottom of the document. But unfortunately, in many situations, this one won’t do.
Technique 3: JavaScript
Managing a CSS transition in CSS would be ideal, but as we’re learning, sometimes it just isn’t entirely possible.
If you absolutely have to have smoothly collapsing sections, whose expanded size is completely driven by their content, and which other elements on the page will flow around as they transition, you can achieve that with some JavaScript.
The basic strategy is to manually do what the browser refuses to: calculate the full size of the element’s contents, then CSS transition the element to that explicit pixel size.
Let’s deconstruct this a little bit. The first thing to note is that we keep track of whether or not the section is currently collapsed using the data-collapsed
attribute. This is necessary so we know what to “do” to the element each time its expansion is toggled. If this were a React or Angular app, this would be a state variable.
The next thing that might stand out is the use of requestAnimationFrame()
. This allows you to run a callback the next time the browser re-renders. In this case, we use it to wait to do something until the style we just set has taken effect. This is important where we change the element’s height from auto
to the equivalent explicit pixels value because we don’t want to wait on a transition there. So we must clear the value of transition
, then set height
, then restore transition
. If these were sequential lines in the code, the result would be as if they’d all been set simultaneously since the browser doesn’t re-render in parallel to Javascript execution (at least, for our purposes).
The other idiosyncrasy is where we set height
back to auto once the expansion has finished happening. We register an event listener with transitionend
, which fires whenever a CSS transition concludes. Inside of that event listener, we remove it (since we only want it to respond to the immediately following transition), then remove height
from the inline styles. This way, the element size is back to being defined however the normal styles for the page define it. We don’t want to assume that it should remain the same pixel size, or even that it should remain auto
sized. We want our JavaScript to perform the transition, and then get out of the way and not interfere more than necessary.
The rest is fairly straightforward. And, as you can see, this achieves exactly the desired result. That said, despite best efforts, there are quite a few ways in which this makes our code more brittle and potentially bug-prone:
- We’ve added 27 lines of code instead of 3
- Changes to things like
padding
orborder-box
in our section element could require changes to this code - CSS transitions on the section, that happen to end while the height transition is still going, could cause height not to be returned to its default value
- Disabling
transition
for one frame could disrupt other transitions on that element which happen to be going at the same time - If a bug ever caused the element’s
height
style to get out of sync with itsdata-collapsed
attribute, its behavior could have problems
On top of all that, the code we’ve written is procedural instead of declarative, which inherently makes it more error-prone and complex. All that said, sometimes our code just needs to do what it needs to do, and if it’s worth the tradeoffs then it’s worth the tradeoffs.
Bonus Technique: Flexbox
I call this technique a bonus because it doesn’t technically achieve the desired behavior. It offers an alternate way of determining your elements’ sizes which in many cases may be a reasonable replacement, and which does fully support transitions.
You may want to read about flexbox and flex-grow before reading this section, if you’re not familiar with them already.
Flexbox is an extremely powerful system for managing the way your interface’s sizing adapts to different situations. Many articles have been written about this, and I won’t go into it in detail. What I will go into, is the lesser-mentioned fact that the flex
property and others related to it fully support transitions!
What this means, is that if your use case allows you to determine sizing using flexbox instead of your content size, making a section smoothly collapse is as simple as setting transition: flex 0.3s ease-out
and toggling flex: 0
. Still not as good as being content-based, but more flexible (I know, I know, I’m sorry) than going to and from pixel sizes.
Nice solutions here, I used to get stuck on this kind of situation.
Actually, I’ve been using a solution that mixes opacity and font-size, if the content is just text it’s easy to control.
All that it needs is to change the font size and the opacity at diferent times: when the content show, it first increases the font size from 0 to the desired and than it changes the opacity to 0; when you need to hide, it first turns transparent and then the font size decreases to 0, working visually smoother :)
Here is a sketch code I used on a previous project: http://codepen.io/pedrorivera/pen/ZpAVwk
font-size
transition causes reflow though.That you for the comprehensive summary of this CSS Transition issue. I’ve used the max-height workaround a few times while thinking there must be a better way. At least now I know that, while there might not be a better way, there are other options to try.
You could make the
transitioning
check more robust by checking the type of transition viatransitionEvent.propertyName
and ignore non-height transitions.Another technique is animating
clip-path
. It doesn’t have universal support yet, but Jake Archibald said Chrome was working on hardware-accelerating it, so it should be an attractive option soon.Great article! I just started getting more into CSS and this is really helpful.
To (maybe) add to or offer an alternative to “Technique 3: JavaScript” above, I’ve been using the following as my go-to for menu/vertical expander transitions. (Specifically with the idea of only using jQuery to add/remove a class – although in this case a little more – rather than jQuery animations, slideToggle, etc.) I welcome any notes on issues/drawbacks anyone sees with it.
CSS:
(The overflow state can be manipulated as needed for multi-level menus, etc. and of course the initial height can be set to auto or some other alternative when JS isn’t present.)
jQuery
(Falls back to instant open/close without JS.)
I’ve tried many of these solutions with “Read More” text, and having to specify a height, or maximum height, doesn’t work because the additional text can be a couple of sentences to a whole page. At least for now I’ve eliminated using transitions with collapsing sections.
The simplest solution is probably that one used e.g. by Bootstrap for mobile menu (if I remember correctly). At least I used the following solution couple of times with a nice result. Just calculate the real height of the toggled element before each toggle and put it into inline style as max-height. The rest is the same – CSS transition between max-height 0 and the one specified inline.
I have used this many times before. It does require that the items be visible when you calculate the heights first tho.
What I do I use a js solution: Before collapsing the element I set its height through element.scrollHeight and then apply a css class with {height: 0px !important; overflow: hidden};
Do you have a codepen of this? I would like to try it out. Sounds interesting.
I’ve used technique 1 myself but instead of hard-coding the max-height I give it a very high max-height and use cubic-bezier easing on the transition to give it a slow to fast easing on hover then fast to slow on unhover.
http://codepen.io/foxareld/pen/XMgMvP
I think it is better to use javaScript (or jQuery) when animating an accordion, because the other css only or JS mixed css options don’t accomplish the goal completely.
jQuery 3.1.1 uses requestAnimationFrame, so animating the height it is sufficiently smooth.
This is the state of the art…
FWIW, there is an issue open in the CSS WG’s github: https://github.com/w3c/csswg-drafts/issues/626. Feel free to give it a thumbs-up if you would like to show support for a native solution in CSS.
Wow, that flex transition trick is awesome. Never really thought about that. So simple :)
Awesome article and documentation Brandon. We just created a package (react-css-collapse) using very much the same techniques as you explain in technique 3. Maybe someone here finds it useful :)
I’ve made some tests too for my accessible scripts, transition on max-height is not perfect, but it does the job (I’ve tested a combination of scaleX and max-height, but didn’t get cool results for the moment). If it becomes too complicated on tiny screens (too much content), you may remove the transition and simply use display: none .
Another keypoint for accessibility (if you want to avoid focusable hidden content): you can’t animate display. Here is the trick, you can animate visibility with a delay (so it will be “animated” at the end of the transition):
Aaron Davidson wrote in with a way to get those height measurements in JavaScript, but then pass the data back to CSS in the form of CSS custom properties for it to transition:
Maurice Carey also stumbled upon the CSS custom properties approach:
Jeroen Sormani wrote in with this example: