The max-height solution is quick and dirty, but it only works in all situations if you set max-height
to a large number, and doing that makes the animation look terrible. The scale solution is also quick and dirty, and looks better, but when collapsed it leaves a big space below it (the content below does not slide up to fill that space as you would expect).
An elegant and flexible solution
I propose using a parent-child relationship. Instead of having a collapsible (an element who's height can change) and the content inside of it, you have a collapsible with an inner-collapsible inside of it, and the content lies inside of the inner-collapsible.
Now here's the trick: you set the inner-collapsible's height
to "auto" (which is the default) so that it can change based on the content inside of it, then you manually set the collapsible's height to be the same height as the inner-collapsible!
the only complication is that if the content changes height for whatever reason, the collapsible and inner-collapsible will no longer be the same height and your solution will break. You can easily solve this by adding a ResizeObserver
to the inner-collapsible so that when it updates, you can update the collapsible to match
A bare example implementation
Here is a barebones implementation that is meant to be copy-pasted into your project, should you need it. This is probably obvious, but you may need to adapt this code to work in your framework and your context.
let collapsibleIsOpen = true;
let innerHeight = 0;
let collapsibleElement = document.querySelector("#collapsible");
let ro = new ResizeObserver(entries => {
innerHeight = entries[0].contentRect.height;
collapsibleElement.style.height = collapsibleIsOpen ? innerHeight + 'px' : 0;
});
ro.observe(document.querySelector("#collapsible-inner"));
function toggleCollapsible(element) {
collapsibleIsOpen = !collapsibleIsOpen;
element.style.height = collapsibleIsOpen ? innerHeight + 'px' : 0;
}
#collapsible {
overflow: hidden;
transition: 0.25s; /* Feel free to change transition to your taste */
}
<div id="collapsible" onclick="toggleCollapsible(this)">
<div id="collapsible-inner">
</div>
</div>
A more fleshed out example
Though the bare implementation above is great for inserting into your code, you can't really use it to see the solution in action. To fix that, I made an example that has some actual content in it, and more content is added after 5 seconds to demonstrate it works dynamically. I also added a little polishing CSS. I wouldn't bother reading this snippet, just run it and click on the collapsible to see it working.
let collapsibleIsOpen = true;
let innerHeight = 0;
let collapsibleElement = document.querySelector("#collapsible");
let ro = new ResizeObserver(entries => {
innerHeight = entries[0].contentRect.height;
collapsibleElement.style.height = collapsibleIsOpen ? innerHeight + 'px' : 0;
});
ro.observe(document.querySelector("#collapsible-inner"));
function toggleCollapsible(element) {
collapsibleIsOpen = !collapsibleIsOpen;
element.style.height = collapsibleIsOpen ? innerHeight + 'px' : 0;
}
// Don't worry about this. This is purely to demonstrate content dynamically changing size
function changeHeightCountdown() {
let timeLeft = 5;
const countdownInterval = setInterval(countdown, 1000);
function countdown() {
timeLeft--;
const newCountdownText = `A new square will change the content's height in ${timeLeft} seconds`;
document.querySelector("#countdown-text").innerHTML = newCountdownText;
if(timeLeft == 0) {
clearInterval(countdownInterval);
let newDiv = document.createElement("div");
newDiv.style.backgroundColor = "magenta";
newDiv.style.width = "64px";
newDiv.style.height = "64px";
document.querySelector("#countdown-text").appendChild(newDiv);
}
}
}
changeHeightCountdown();
#collapsible {
overflow: hidden;
transition: 0.25s;
border: 2px solid black;
}
#collapsible:hover {
cursor: pointer;
}
<div id="collapsible" onclick="toggleCollapsible(this)">
<div id="collapsible-inner">
<span>Example content</span>
<div style="width: 64px; height: 64px; background-color: cyan;"></div>
<span id="countdown-text">A new square will change the content's height in 5 seconds</span>
</div>
</div>