No CSS transition for 'height: fit-content'
Asked Answered
T

9

32

I use transition: height 500ms to add an animation to an element that slides open via button from height: 0 to height: 100px and vice versa.

Since the element's content is added dynamically and I don't know its size I would like to switch to height: fit-content instead. This way the element would have always the right size to show its content.

Sadly this disables the animation.

How can I get the animation together with a div element which size fits its content?

Following snippet shows the behavior:

document.querySelector('button')
  .addEventListener(
    'click',
    () => document.querySelectorAll('div')
      .forEach(div => div.classList.toggle('closed')));
div {
  background-color: lightblue;
  border: 1px solid black;
  overflow: hidden;
  transition: height 500ms;
}

div.closed {
  height: 0 !important;
}

div.div1 {
  height: 100px;
}

div.div2 {
  height: fit-content;
}
<button type="button">toggle</button>

<h1>'height: 100px' => 'height: 0'</h1>
<div class="div1">
some text<br />
even more text<br />
so much text
</div>

<br>

<h1>'height: fit-content' => 'height: 0'</h1>
<div class="div2">
some text<br />
even more text<br />
so much text
</div>
Thekla answered 14/9, 2018 at 17:52 Comment(3)
fit-content is actually a function which IIRC is not an animate-able value.Decurion
There's no elegant pure CSS solution, jQuery can solve it with ease (.slideToggle()).Bawbee
Possible duplicate of How can I transition height: 0; to height: auto; using CSS?Bawbee
S
12

As Mishel stated another solution is to use max-height. Here is a working example of that solution.

The key is to approximate your max-height when it is fully expanded, then the transitions will be smooth.

Hope this helps.

https://www.w3schools.com/css/css3_transitions.asp

document.querySelector('button')
  .addEventListener(
    'click',
    () => document.querySelectorAll('div')
      .forEach(div => div.classList.toggle('closed')));
div {
  background-color: lightblue;
  border: 1px solid black;
  overflow-y: hidden;
	max-height: 75px; /* approximate max height */
	transition-property: all;
	transition-duration: .5s;
	transition-timing-function: cubic-bezier(1, 1, 1, 1);
}
div.closed {
   max-height: 0;
}
<button type="button">toggle</button>

<h1>'height: 100px' => 'height: 0'</h1>
<div class="div1">
some text<br />
even more text<br />
so much text
</div>

<br>

<h1>'height: fit-content' => 'height: 0'</h1>
<div class="div2">
some text<br />
even more text<br />
so much text
</div>
Shanelleshaner answered 14/9, 2018 at 19:23 Comment(3)
Thanks! This solution fits my situation and goal best.Thekla
As an improvement I use max-height: 100vh; instead. 100vh means 100% of available view height (the size of the actual content area of the browser window). My element is always much smaller than 100vh.Thekla
Good to hear, glad I could help.Shanelleshaner
H
5

one possible solution, though not perfect, is to animate font-size instead of height.

another solution might be to animate max-height instead of height. you can use max-height say 300px or 500px. but if you require more than that, it won't look good.

here I'm animating font-size.

Hope that helps. Thanks.

document.querySelector('button')
  .addEventListener(
    'click',
    () => document.querySelectorAll('div')
      .forEach(div => div.classList.toggle('closed')));
div {
  background-color: lightblue;
  border: 1px solid black;
  overflow: hidden;
  transition: font-size 500ms;
}

div.closed {
  font-size: 0 !important;
}

div.div1 {
  font-size: 14px;
}

div.div2 {
  font-size: 14px;
}
<button type="button">toggle</button>

<h1>'height: 100px' => 'height: 0'</h1>
<div class="div1">
some text<br />
even more text<br />
so much text
</div>

<br>

<h1>'height: fit-content' => 'height: 0'</h1>
<div class="div2">
some text<br />
even more text<br />
so much text
</div>
Hostetler answered 14/9, 2018 at 19:3 Comment(2)
I indeed used this method for another situation where the content were elements itself: As you are scaling the text element I scaled the child elements. Thanks for the reminder!Thekla
Works perfectly for my dropdown menus of various heights. Thanks!Cimabue
A
3

Considering that heights can vary considerably between mobile and desktop versions of the site, I didn't like the max-height solution for transitioning height from 0 to fit-content because if I underestimated it would cut content off, and if I overestimated the transition looked weird.

Here's my combined CSS + JS solution:

//CSS
div {
    overflow: hidden;
    transition: all 220ms;
}    

div:not(.active) {
    height: 0px !important;
}


//JS
div.addEventListener('click', function(e) {
   div.style.height = `${div.scrollHeight}px`;            
   div.classList.add('active');
});
Alba answered 14/5, 2023 at 22:14 Comment(1)
Definitely the easiest to cobble together in most any language and frameworkVelate
C
1

I made a solution mixing filter and transform properties, which achieves a good looking effect:

    .dropdown-content {
        width: fit-content;
        transform: scaleY(0);
        filter: brightness(1000%);
        transform-origin: top;
        transition: all 0.5s;
    }
    .dropdown-content.visible {
        transform: scaleY(1);
        filter: none;
    }

This eliminates the effect of shrinking that scaleY() gives. The class "visible" should be toggled via JavaScript

Collazo answered 5/7, 2023 at 15:15 Comment(0)
C
0

The way I solved my problem was Calculating the height of the children

document.querySelector('.button1').addEventListener('click',
  () => document.querySelector('.div1').classList.toggle('closed'));

document.querySelector('.button2').addEventListener('click',
  () => document.querySelector('.div2').classList.toggle('closed'));

let extendedHeight = 0;
let div2 = document.querySelector('.div2')
for (let i = 0; i < div2.children.length; i++) {
  extendedHeight += div2.children[i].offsetHeight;
}

div2.style.setProperty('--extended-height', extendedHeight.toString() + "px")
div {
  background-color: lightblue;
  border: 1px solid black;
  overflow: hidden;
  transition: 500ms;
}

div.div1.closed {
  max-height: 0 !important;
}

div.div1 {
  max-height: 1000px;
}

div.div2.closed {
  height: 0 !important;
}

div.div2 {
  --extended-height: 0px;
  height: var(--extended-height);
}
<button type="button" class="button1">toggle</button>

<h1>'height: over predicted' => 'height: 0'</h1>
<div class="div1">
  <p>
  some text <br> even more text <br> so much text <br><br>
  </p>
</div>

<button type="button" class="button2">toggle</button>

<h1>'height: calculated' => 'height: 0'</h1>
<div class="div2">
  <p>some text <br> even more text <br> so much text <br><br>
  </p>
</div>

If the height is over predicted then the animation will look broken, but if the height is perfectly calculated and set the animation will work correctly and look perfect.

To do this I calculated the height of all of the children of the div using the offsetHeight which returns the height of the element + padding-top + border-top + margin-top

I then set the custom CSS variable that I made which is --extended-height to the height of all the children

You might get a problem if the div has different elements that are positioned beside each other, and the way I solved that was only getting the offsetHeight of the biggest elements of the elements that are positioned beside each other. In my case I just used the offsetHeight of every other element

Thanks

Clara answered 1/1, 2023 at 11:26 Comment(0)
R
0

you can try using tranform: scaleY(0) -> scale(1) its kinda does the same thing as height.

Resolved answered 2/2, 2023 at 0:7 Comment(0)
I
0

I use this:

function slide(button, event) {
            document.getElementById("container").classList.toggle("container-opened");
        }
.container {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    height: 0;
    overflow: hidden;
    transition: 1s all;
}

.card {
    margin: 10px 5%;
    padding: 10px;
    background-color: #CCC;
}

.container-opened {
    height: 150vh;
    box-shadow: 1px 1px 10px 1px #CCC;
}
<button onclick="slide(this, event)">Slide!!</button>
    <div id="container" class="container">
        <article class="card">
            <p>I'm a children!!</p>
        </article>

        <article class="card">
            <p>I'm a children!!</p>
        </article>

        <article class="card">
            <p>I'm a children!!</p>
        </article>

        <article class="card">
            <p>I'm a children!!</p>
        </article>
    </div>

Just make sure the height is greater than the content itself occupies, and the content will automatically distribute itself. This can also be used with flex-direction: row, manipulating width instead of height, just making small changes to justify-content and align-items

Incudes answered 3/4, 2023 at 19:2 Comment(0)
G
0

use the max height like others are saying but just make max-height: fit-content; and it should work as expected.

Greenlet answered 30/6, 2023 at 1:22 Comment(3)
and use a percent for the height instead of pxGreenlet
height: 0% max-height: fit-content hover{ height: 100%Greenlet
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Stere
S
0

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>
Sardella answered 16/5 at 17:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.