Search code examples
cssanimationcss-animationscss-transitionscss-transforms

How to animate height with transform without squishing content


In my project I have been using jQuery slideUp() to slide up an element in a 200 item list when the user clicks a button. But, as everyone knows, animating CSS height requires a reflow, making the animation jerky. This animation is an integral part of the application and I am willing to go to extensive work to make it work AND work smoothly.

I have decided that CSS transform is the way to make it work smoothly because of the fact that it is handled on the gpu and on some modern browsers, it is even off the main thread and heavy JS work won't affect the transform. (I do have heavy JS work).

I am looking for a clever solution with CSS transition: transform to replicate jQuery slideUp, which just animates the height property. Below was my attempt, but it seems scale and translate do not sync as expected.

$("button").on("click", function() {
  $(".collapsible").addClass("collapsed")
  setTimeout(function() {
    $(".collapsible").removeClass("collapsed")
  }, 5000);
});
.list-item {
  width: 400px;
  height: 100px;
  background-color: blue;
  margin-top: 10px;
  overflow-y: hidden;
}

.collapsible.collapsed .content {
  transition: transform 3s linear;
  transform: scale(1, 1) translate(0, -100%);
}

.collapsible.collapsed {
  transition: transform 3s linear;
  transform: translate(0, -50%) scale(1, 0);
}

.collapsible.collapsed ~ .list-item {
  transition: transform 3s linear;
  transform: translate(0, -100%);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<button>Collapse</button>
<div class="list-item collapsible">
  <div class="content">
    Just an example <br>
    Just an example <br>
    Just an example <br>
    Just an example <br>
    Just an example <br>
  </div>
</div>
<div class="list-item">
  
</div>
<div class="list-item">
  
</div>

I played with the values some and got it closer by changing the content transform to transform: scale(1, 3) translate(0, -50%);.

It seems I am so close to achieving, but never quite succeeding. Is there any cut and dried trick out there for this?

Requirements:

  • Preferably no JS
  • Off the main thread

Solution

  • With a few nights of sleep, I landed on a solution that works precisely as I needed. I had to utilize three layers of divs and make sure the inner two are set to height: 100%;. I then do away with scale() and use transform() to slide the middle layer up.

    This meets my requirements:

    • Uses Transform() which should run on the GPU up to 60fps
    • Doesn't squish content with Scale()
    • Content disappears from bottom to top like jQuery SlideUp()
    • Other elements below slide up to fill the gap no matter the height of the target element
    • Once animation is triggered there is no more JS involved

    $("button").on("click", function() {
      const height = $(".collapsible").css("height");
      $(":root").css("--height", height);
      $(".collapsible").addClass("collapsed");
      setTimeout(function() {
        $(".collapsible").removeClass("collapsed");
      }, 4000);
    });
    :root {
      --height: unset;
    }
    
    .outer-container {
      width: 400px;
      height: fit-content;
      background-color: transparent;
      margin-top: 10px;
      overflow-y: hidden;
    }
    
    .inner-container {
      height: 100%;
      background-color: blue;
      overflow: hidden;
    }
    
    .content {
      height: 100%;
    }
    
    .collapsible.collapsed .inner-container {
      transition: transform 3s linear;
      transform: translate(0, -100%);
    }
    
    .collapsible.collapsed .content {
      transition: transform 3s linear;
      transform: translate(0, 100%);
    }
    
    .collapsible.collapsed ~ .outer-container {
      transition: transform 3s linear;
      transform: translate(0, calc((var(--height) * -1) - 10px));
    }
    
    .collapsible .inner-container {
      transition: transform .5s ease-in-out;
      transform: translate(0, 0);
    }
    
    .collapsible .content {
      transition: transform .5s ease-in-out;
      transform: translate(0, 0);
    }
    
    .collapsible ~ .outer-container {
      transition: transform .5s ease-in-out;
      transform: translate(0, 0);
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
    <button>Collapse</button>
    <div class="outer-container">
      <div class="inner-container">
        <div class="content">
          Just an example <br>
          Just an example <br>
          Just an example <br>
          Just an example <br>
        </div>
      </div>
    </div>
    <div class="outer-container collapsible">
      <div class="inner-container">
        <div class="content">
          Just an example <br>
          Just an example <br>
          Just an example <br>
          Just an example <br>
          Just an example <br>
        </div>
      </div>
    </div>
    <div class="outer-container">
      <div class="inner-container">
        <div class="content">
          Just an example <br>
          Just an example <br>
          Just an example <br>
        </div>
      </div>
    </div>
    <div class="outer-container">
      <div class="inner-container">
        <div class="content">
          Just an example <br>
          Just an example <br>
          Just an example <br>
          Just an example <br>
        </div>
      </div>
    </div>

    Note: for my particular use case, I made the following div slide up far enough to cover the margin. I believe this is how a jQuery slideUp() function works.

    EDIT: Updated snippet so it works with items of different heights by using a css variable and setting it with JS.