23

I'm using CSS transitions with the transform property to shrinks elements when adding and removing them.

However one problem with this is that this property doesn't affect the flow of other elements, so it appears as though the element being deleted shrinks away, and then the rest of the elements jump suddenly.

If I were to animate the height property instead of using a transform this would be fine, however in actual usage I am using elements of variable height so I won't know what heights I can animate between.


Edit: people have suggested animating the height property (which won't work as stated above), or the max-height property. The max-height property will work to some extent, however you cannot align the timings perfectly as the transition will keep adjusting the max-height property past the actual height of the element until the end of the transition time.

Another problem with these approaches is that it does not use the smooth animations that you can achieve with the transform property. The object's transform will happen smoothly, however the movement of the following elements will stutter as the browser renders these transitions differently.


Here's a JSFiddle with what I mean (try adding then removing elements, and see the jump when elements are removed):

var button = document.querySelector("button");
var box = document.createElement("div");

box.className = "box";
box.appendChild(document.createTextNode("Click to delete"));

button.addEventListener("click", function(e) {
  var new_box = box.cloneNode(true);

  new_box.addEventListener("click", function(e) {
    this.className = "box deleting";
    window.setTimeout(function(e) {
      new_box.remove();
    }, 1000);
  });

  this.parentNode.appendChild(new_box);
});
button {
  font-size: 20pt;
}
.box {
  font-size: 20pt;
  margin: 10px;
  width: 200px;
  padding: 10px;
  background: pink;
  transform: scale(1, 1);
  transform-origin: top left;
}
.deleting {
  transform: scale(1, 0);
  transition: all 1000ms ease;
}
<button>
  Add Box
</button>
Lauren
  • 1,480
  • 1
  • 13
  • 36

5 Answers5

4

For elements whose layout is governed by the CSS box model, the transform property does not affect the flow of the content surrounding the transformed element.

REF: Transform Rendering

You will have to use the max-height property (check this answer) to get the desired effect.

var button = document.querySelector("button");
var box = document.createElement("div");

box.className = "box";
box.appendChild(document.createTextNode("Click to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to deleteClick to delete"));

button.addEventListener("click", function(e) {
  var new_box = box.cloneNode(true);
  new_box.style.height = ( Math.random() * (200 - 30) + 30 ) + 'px';

  new_box.addEventListener("click", function(e) {
    this.className = "box deleting";
    window.setTimeout(function(e) {
      new_box.remove();
    }, 1000);
  });

  this.parentNode.appendChild(new_box);
});
button {
  font-size: 20pt;
}
.box {
  overflow:hidden;
  font-size: 12pt;
  margin-bottom: 10px;
  width: 600px;
  max-height:1000px;
  padding: 10px;
  background: pink;
  transform: scaleY(1);
  transform-origin: top left;
}
.deleting {
  transform: scaleY(0);
  max-height:0;
  padding:0 10px;
  margin-bottom:0;
  transition: padding 1000ms ease,max-height 1000ms ease, transform 500ms ease 500ms, margin-bottom 1000ms ease;
}
<button>
  Add Box
</button>
Community
  • 1
  • 1
sabithpocker
  • 15,274
  • 1
  • 42
  • 75
  • Not downvoting, but did you test your answer? It's janky at best. – Ryan Wheale Aug 27 '16 at 19:42
  • @RyanWheale This actually works fine for elements with known height range, like 500px to 800px, the problem is that animation time will depend on the max-height applied. Just wanted to point out this possibility :) – sabithpocker Aug 27 '16 at 20:43
  • Ok, I have made some modifications to demonstrate the solution! But again this works only for certain scenarios :) – sabithpocker Aug 27 '16 at 20:56
  • Hi @sabithpocker - your solution seems to work pretty well. Could you explain for me (or in the answer) why you've used different transition durations for `max-height` and the reason for the 500ms delay on the `transform` transition? – Lauren Aug 30 '16 at 14:19
  • 1
    @Lauren Without the delay transform transition happens initially and that forces the height to remain constant until the transition ends, the brute force idea is just trying to delay that so that height transition happens from the beginning. Try removing the delay, it will animate scaleY and then animate height, not both together. All the values need to be tuned to fit your requirements! – sabithpocker Aug 31 '16 at 14:25
1

There's no way to do it right (with css alone) ¬¬

The way I did it was partially using JS (or C#/Blazor in my case)

I used the max-height but I set it with a var(), and the value itself I'm calculating based on the content that I have (I'm using JS/c# for that), then use that value to set an inline style withe the css variable:

<ul 
    class="submenu @SubMenuActiveClass" 
    style="--max-height:@((int)(40.3f * Model.Children.Count))px">

    <!--... childrens with 40.3px height each -->

</ul>
.submenu {
    max-height: 0;
    transition: max-height 0.3s ease-out;
    overflow: hidden;

    &.active {
        max-height: var(--max-height);
    }
}

The example is in Blazor and SCSS but it's quite easy to understand

0

You could create a wrapper around the shrinking boxes. Now you're just scaling the red boxes, that means it still uses the same space, but it's rendered in a scaled way. When you finally delete it, it doesn't use that space anymore, the elements under it will jump upwards.

If you create a wrapper around each red box, you can control the space it takes. Just change the height of that wrapper, with a transition too.

/* css for a wrapper */
overflow: hidden;
transition: height .2s; /* your time */

So, don't only scale the red boxes with a transition, but also put them in a wrapping element, and change that element's height.

user2190492
  • 1,174
  • 2
  • 9
  • 37
0

For the best affect, you will need to modify the boxing properties which affect its surroundings. This includes the width/height, padding, margins, and border width. In the below example, I set the relevant properties to 0 for your desired effect. This includes all properties which affect the vertical spacing of the element: height, padding-[top/bottom], margin-[top/bottom], and border-[top/bottom]-width are all transitioned to 0.

I also added box-sizing: border-box; and overflow: hidden; to the box - you should see what happens when those are not set. I'll let you learn about those on your own.

var button = document.querySelector("button");
var box = document.createElement("div");

box.className = "box";
box.appendChild(document.createTextNode("Click to delete"));

button.addEventListener("click", function(e) {
  var new_box = box.cloneNode(true);

  new_box.addEventListener("click", function(e) {
    new_box.style.height = this.offsetHeight + 'px';
    window.requestAnimationFrame(function () {
      new_box.style.height = 0;
      new_box.className = "box deleting";
      window.setTimeout(function(e) {
        new_box.remove();
      }, 1000);
    });
  });

  this.parentNode.appendChild(new_box);
});
button {
  font-size: 20pt;
}
.box {
  box-sizing: border-box;
  overflow: hidden;
  font-size: 20pt;
  margin: 10px;
  width: 200px;
  padding: 10px;
  border: 5px solid red;
  background: pink;
  transform: scale(1, 1);
  transform-origin: top left;
}
.deleting {
  height: 0;
  padding-top: 0;
  padding-bottom: 0;
  margin-top: 0;
  margin-bottom: 0;
  border-top-width: 0;
  border-bottom-width: 0;
  transform: scale(1, 0);
  transition: all 1000ms ease;
}
<button>
  Add Box
</button>
Ryan Wheale
  • 26,022
  • 8
  • 76
  • 96
  • Hi Ryan, thanks for your answer. This would work for fixed height elements, however it doesn't work for elements of variable height. – Lauren Aug 27 '16 at 19:23
  • Thanks @Lauren - I figured with buttons, there's not much harm in a fixed hight. You didn't ask for a universal solution, but you are absolutely right - I failed to mention the fixed height requirement in my original answer. I have updated it to no longer require a fixed height. – Ryan Wheale Aug 27 '16 at 19:40
  • 5
    Also worth mentioning, when you start having animations which are attached to your app state, you can very quickly get in to trouble by mixing in css animations. There are all sorts of timing issues, order of precedence issues, and [worst of all] maintenance issues. For example, what if you want the duration to be 500ms, you have to change it in two places - not good. I have found after 10 years that these types of animations should be done in JavaScript only. CSS animations are good for real simple things which aren't tied to your application state. – Ryan Wheale Aug 27 '16 at 19:48
  • This solution is certainly more promising but for the reasons you've laid out in your follow-up comment probably wouldn't be practical. For simplicity's sake I gave an example in pure JS, in reality this situation is in a larger application where it's non-trivial to inject animation frame callbacks that mess with the properties of the object. (You may be right, I should look into pure JS solutions). – Lauren Aug 27 '16 at 19:52
  • The decisions you make with your app shouldn't affect whether I answered your question ;) Animation frameworks do all sorts of craziness, setting properties on your objects, "injecting animation frames" as you put it, and much more - there's no way around it, they just abstract it for you. Most of them provide an API for "resetting" any side effects after the animation is done. There is no way to do what you want without JS, and it sounds very much like you need an animation framework. I recommend GSAP. – Ryan Wheale Aug 27 '16 at 20:04
  • Thank-you for your advice. I will certainly accept your answer if no preferable alternative is posted soon. – Lauren Aug 27 '16 at 20:13
0

The solution is to animate margin into negative values.

var button = document.querySelector("button");
var box = document.createElement("div");

box.className = "box";
box.appendChild(document.createTextNode("Click to delete"));

button.addEventListener("click", function(e) {
  var new_box = box.cloneNode(true);

  new_box.addEventListener("click", function(e) {
    this.className = "box deleting";
    window.setTimeout(function(e) {
      new_box.remove();
    }, 1000);
  });

  this.parentNode.appendChild(new_box);
});
button {
  font-size: 20pt;
}
.box {
  box-sizing: border-box;
  font-size: 20pt;
  margin: 10px;
  width: 200px;
  padding: 10px;
  background: pink;
  transform: scale(1, 1);
  transform-origin: top left;
  height: 2em;
}
.deleting {
  transform: scale(1, 0);
  margin-bottom: calc(-2em - 10px);
  transition: all 1000ms ease;
}
<button>
  Add Box
</button>
elixon
  • 1,123
  • 12
  • 15