Is it possible to animate Flexbox inserts & removes?

CssCss TransitionsCss AnimationsFlexbox

Css Problem Overview


When I remove an item from a flexbox, the remaining items "snap" into their new positions immediately rather than animating.

Conceptually, since the items are changing their positions, I would expect the transitions to apply.

I have set the transition property on all involved elements (the flexbox and the children)

Is there any way to animate edits (adds & deletes) to a flexbox? This is actually a showstopper for me and the one missing piece with flexbox.

Css Solutions


Solution 1 - Css

I've fixed up @skyline3000's demo based on this example from Treehouse. Not sure if this will break again if browsers change but this seems to be the intended way to animate flex size changes:

http://jsfiddle.net/2gbPg/2/

Also I used jQuery but it technically isn't required.

.flexed {
    background: grey;
    /* The border seems to cause drawing artifacts on transition. Probably a browser bug. */
    /* border: 1px solid black; */
    margin: 5px;
    height: 100px;
    flex-grow: 1;
    transition: flex-grow 1000ms linear;
}

.removed {
    /* Setting this to zero breaks the transition */
    flex-grow: 0.00001;
}

One thing to note about the CSS is you can't transition to a flex-grow of zero, it won't transition it will just disappear. You need to just put a very small value. Also there seems to be an artifacting bug when drawing borders so I've used a background in this case.

Solution 2 - Css

Remember that the Flexible Box Model and Grid Layout specifications are changing constantly, even the properties and valid values. The browser implementations are far from complete as well. That being said, you can transition on the flex property so that the elements transition smoothly, then just listen for TransitionEnd to finally remove the node from the DOM tree.

Here is an example JSFiddle, running in Chrome 21: http://jsfiddle.net/5kJjM/ (click the middle div)

var node = document.querySelector('#remove-me');

node.addEventListener('click', function(evt) {
  this.classList.add('clicked');
}, false);

node.addEventListener('webkitTransitionEnd', function(evt) {
  document.querySelector('#flexbox').removeChild(this);
}, false);

#flexbox {
  display: -webkit-flex;
  -webkit-flex-flow: row;
}

.flexed {
  border: 1px solid black;
  height: 100px;
  -webkit-flex: 1 0 auto;
  -webkit-transition: -webkit-flex 250ms linear;
}

.clicked {
  -webkit-flex: 0 0 auto;
}

<div id="flexbox">
  <div class="flexed"></div>
  <div class="flexed" id="remove-me"></div>
  <div class="flexed"></div>
</div>

Edit: To further clarify, when you remove a node, you should set its flex to 0, then remove it from the DOM. When adding a node, add it in with flex: 0, then transition it to flex:1

Solution 3 - Css

I've done a codepen that animates the elements when you remove one, take a look: https://codepen.io/MauriciAbad/pen/yLbrpey

HTML

<div class="container">
    <div></div>
    <div></div>
    ... more elements ...
</div>

CSS

.container{
    display: flex;
    flex-wrap: wrap;
}
.container > * {
    transform-origin: left top;
}

TypeScript

If you want the JavaScript just remove the : Anything from the function's signature and the interface at the top.

interface FlexItemInfo {
  element: Element

  x: number
  y: number
  width: number
  height: number
}

const container = document.querySelector('.container')
for (const item of container.children) {
  item.addEventListener('click', () => {
    removeFlexItem(container, item)
  })
}

function removeFlexItem(container: Element, item: Element): void {
  const oldFlexItemsInfo = getFlexItemsInfo(container)
  container.removeChild(item)
  const newFlexItemsInfo = getFlexItemsInfo(container)

  aminateFlexItems(oldFlexItemsInfo, newFlexItemsInfo)
}

function getFlexItemsInfo(container: Element): FlexItemInfo[] {
  return Array.from(container.children).map((item) => {
    const rect = item.getBoundingClientRect()
    return {
      element: item,
      x: rect.left,
      y: rect.top,
      width: rect.right - rect.left,
      height: rect.bottom - rect.top,
    }
  })
}

function aminateFlexItems(
  oldFlexItemsInfo: FlexItemInfo[],
  newFlexItemsInfo: FlexItemInfo[]
): void {
  for (const newFlexItemInfo of newFlexItemsInfo) {
    const oldFlexItemInfo = oldFlexItemsInfo.find(
      (itemInfo) => itemInfo.element === newFlexItemInfo.element
    )

    const translateX = oldFlexItemInfo.x - newFlexItemInfo.x
    const translateY = oldFlexItemInfo.y - newFlexItemInfo.y
    const scaleX = oldFlexItemInfo.width / newFlexItemInfo.width
    const scaleY = oldFlexItemInfo.height / newFlexItemInfo.height

    newFlexItemInfo.element.animate(
      [
        {
          transform: `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`,
        },
        { transform: 'none' },
      ],
      {
        duration: 250,
        easing: 'ease-out',
      }
    )
  }
}

Solution 4 - Css

I accidentally got it to work in a simple way. Basically, you set width:0;flex-grow:1 and of course add transition:all 2s; and that's it. It's a curious hack.

See it working

Solution 5 - Css

Another adaptation to @skyline3000 and @chris-nicola's solutions: http://jsfiddle.net/2gbPg/2/

You can simply animate max-width (or max-height as appropriate), animating to 0 when removing, and 100% when inserting.

Solution 6 - Css

I have been trying to animate rows in my flexbox. This was not possible through just css. So I did it with a bit of javascript and an extra parent for my flexbox rows.

HTML :

<div class="outer-container">
  <div class="inner-container">
    <div class="row">Row number 1</div>
  </div>
</div>

<button id="add">Add one more item</button>

CSS :

.row{
  height : 40px;
  width : 200px;
  border: 1px solid #cecece;
  text-align : center;
  padding-top : 20px;
}

.outer-container {
  height : 500px;
  border : 1px solid blue;
  width : 250px;
  display: flex;
  justify-content : center;
  align-items: center;
}

.inner-container { 
  height : 42px;
  transition : height 0.3s;
 }

button {
  width : 200px;
  height: 30px;
  margin-left : 80px;
  margin-top : 10px;
}

Javascript :

(() => {
    let count = 1;
    document.querySelector("#add").addEventListener('click', () => {
        const template = document.createElement('div');
        count += 1;
        template.textContent = `Row number ${count}`;
        template.className = 'row';
      
        const innerContainer = document.querySelector('.inner-container');
        innerContainer.appendChild(template);
        innerContainer.style.height = `${innerContainer.clientHeight + 42}px`;
    })
})();

Working demo : https://jsfiddle.net/crapbox/dnx654eo/1/

Solution 7 - Css

You can use MutationObserver to start an animation, when a child has been added or removed.

The advantage is, that you don't have to modify any existing code, including SPA frameworks like Angular, React or Blazor (even server side). You don't have to add artificial animation classes.

It should work for any layout, not just flexbox.

Following is based on nice Maurici Abad's answer using MutationObserver:

//add this to your project

function animateChildren(container) {
  function getFlexItemsInfo(container) {
    return Array.from(container.children).map((item) => {
      const rect = item.getBoundingClientRect()
      return {
        element: item,
        x: rect.left,
        y: rect.top,
        width: rect.right - rect.left,
        height: rect.bottom - rect.top,
      }
    })
  }

  function animateFlexItems(oldFlexItemsInfo,  newFlexItemsInfo) {
    for (const newFlexItemInfo of newFlexItemsInfo) {
      const oldFlexItemInfo = oldFlexItemsInfo.find(e => e.element == newFlexItemInfo.element);

      if (!oldFlexItemInfo) {
        continue; 
      }

      const translateX = oldFlexItemInfo.x - newFlexItemInfo.x
      const translateY = oldFlexItemInfo.y - newFlexItemInfo.y
      const scaleX = oldFlexItemInfo.width / newFlexItemInfo.width
      const scaleY = oldFlexItemInfo.height / newFlexItemInfo.height


      newFlexItemInfo.element.animate(
        [
          {
            transform: `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`,
          },
          { transform: 'none' },
        ],
        {
          duration: 250,
          easing: 'ease-out',
        }
      )
    }
  }
  
  
  let oldFlexItemsInfo = getFlexItemsInfo(container);
  // Callback function to execute when mutations are observed
  const childListMutationCallback = function(mutationsList, observer) {
      const newFlexItemsInfo = getFlexItemsInfo(container);
      if (oldFlexItemsInfo) {
        animateFlexItems(oldFlexItemsInfo, newFlexItemsInfo);  
      }
      oldFlexItemsInfo = newFlexItemsInfo;
  };

  new MutationObserver(childListMutationCallback).observe(container, { childList: true });
}


  
const container = document.querySelector('.container');
animateChildren(container);

//emulate existing adding/removing items
document.addEventListener('click', e => {
  const item = e.target.closest('.item');
  if (item) {
    e.target.matches('.btn-close') 
      ? container.removeChild(item)
      : container.prepend(item.cloneNode(true));
  }
});

.container {
  display: flex;
  flex-wrap: wrap;
  gap: 2em;
  width: 100%;
  overflow: none;
}
.container>* {
  transform-origin: left top;  
}
  
.container>* {
  flex-grow: 1;
  max-width: 50%;
  min-width: 20%;
  border-radius: 5px;
  height: 5em;
  margin: 0.5rem;
  box-shadow: 0 1px 8px rgba(0,0,0,0.3);
  padding: 1em;
  background: white;
}

.btn-close:after {
  padding: .25em .25em;
  content: 'X';
  border: solid 1px
}

<div class="container">
  <div class="item"> 
    Item 1 <span class="btn-close"></span>
  </div>
  <div class="item" style="background: #f64f59"> 
    Item 2 <span class="btn-close"></span>
  </div>
  <div class="item"  style="background: #c471ed ">
    Click to add <span class="btn-close"></span>
  </div>
</div>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmmaclaurinView Question on Stackoverflow
Solution 1 - CssChris NicolaView Answer on Stackoverflow
Solution 2 - Cssskyline3000View Answer on Stackoverflow
Solution 3 - CssMaurici AbadView Answer on Stackoverflow
Solution 4 - CssJohn Balvin AriasView Answer on Stackoverflow
Solution 5 - CssAlecView Answer on Stackoverflow
Solution 6 - CssVishnu MohanView Answer on Stackoverflow
Solution 7 - CssLieroView Answer on Stackoverflow