Is it possible to animate Flexbox inserts & removes?
Asked Answered
S

8

76

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.

Saransarangi answered 19/6, 2012 at 18:14 Comment(2)
Are you able to post a demo that reproduces your problem at all? Either JS Fiddle, JS Bin or similar would be good, for us to see what's going on in your code without having to built our own tests.Marinna
try (for chrome) -webkit-transition:width 2s; I think it may be the width which is animating when the box is removed.Weidman
O
30

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.

Opine answered 26/7, 2014 at 22:33 Comment(5)
What To Do For Animate Justify-Content from center to flex-startAmaty
I'm not exactly sure I understand what you mean, can you post an example?Opine
is it possible to do this with a vertical layout? i.e. flex-flow: column;Houck
This only seems to work if theres no content, which seems...unlikely. (ex. put some text in the div)Meridithmeriel
It does not work neither when the element it's an imageBiology
S
15

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

Silas answered 3/7, 2012 at 16:7 Comment(4)
where there is content in your middle div, this doesn't quite work as wellDocilla
You could easily wrap the content and set overflow to be hidden during the transition so that it works nicely.Silas
i can't find any transition in the demo... it works just like normal flex... and the transitionEnd handler in the demo is not triggering which means no transitions are being applied...Dactylology
Yeah same problem. If this was working it no longer is.Opine
S
15

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',
      }
    )
  }
}
Straus answered 4/12, 2018 at 17:35 Comment(3)
This is a pretty neat solution. One thing i'm not able to understand is that you are calling moveCards after card.parentNode.removeChild(card). Still the repositioning of remaining elements is not done until animation starts. Shouldn't the elements be first re-positioned immediately in flex container (due to element removal) and then transition should start? I wonder why this is able to work like it does!Rink
@Rink Yes. The elements move into the new position, and then the animation moves it back to where it was to the current position. There's a small gap between the element removal and the animation starting, but js is so fast and the browser optimized that it looks fluid.Straus
Understood. But why does .animate move them back and then start animating? We are translating them and not changing their left/right values, so if they have moved to new positions after element removal, shouldn't they be translated from those new positions?Rink
B
7

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

Biology answered 5/6, 2018 at 8:7 Comment(1)
width trick worked for me thanks!!Tedford
B
4

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.

Boohoo answered 6/4, 2020 at 13:14 Comment(0)
M
3

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>
Marcello answered 27/12, 2021 at 14:24 Comment(2)
can this work for grid?Diesel
it should, yes..Marcello
S
1

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/

Styracaceous answered 11/2, 2019 at 17:15 Comment(0)
S
0

I've created a codepen that animates the flexbox inserts and removes. https://codepen.io/raj_srikar/pen/dyLPRje

This creates 3 boxes with random colors in both of the flexbox containers. On clicking a box, it'll be smoothly removed from its container and will move across the screen and gets inserted at the beginning of the other container by smoothly adjusting the other boxes.

HTML:

<div id="containerA" class="flex-container"></div>
<div id="containerB" class="flex-container"></div>

CSS:

.flex-container {
  display: flex;
  flex-wrap: wrap;
  justify-content: center; /* Center items horizontally */
  align-items: center; /* Center items vertically */
  padding: 10px;
  border: 2px dashed #ccc;
  margin-bottom: 20px;
  min-height: 150px;
}

.color-box {
  width: 100px;
  height: 100px;
  cursor: pointer;
  transition: margin 0.5s ease-in-out; /* Animate margin changes */
  flex-shrink: 0; /* Prevent boxes from shrinking */
}

.abs{
  position:absolute;
}

JavaScript:

var transDur = 500;

// Function to generate random color
function getRandomColor() {
  const letters = '0123456789ABCDEF';
  let color = '#';
  for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)];
  }
  return color;
}

// Function to create a color box
function createColorBox(container) {
  const box = document.createElement('div');
  box.className = 'color-box';
  box.style.backgroundColor = getRandomColor();
  container.appendChild(box);
  box.addEventListener('click', () => {
    const sourceContainer = box.parentElement;
    const targetContainer = sourceContainer === containerA ? containerB : containerA;
    moveBox(box, sourceContainer, targetContainer);
  });
}

// Function to move the box to the other container
function moveBox(box, sourceContainer, targetContainer) {
  // Create a placeholder to reserve space in the target container
  const destDummy = document.createElement('div');
  destDummy.className = 'color-box';
  destDummy.style.visibility = 'hidden'; // Hide the placeholder
  destDummy.style.transition = `width ${transDur}ms ease-in-out`;
  // Create a placeholder to unreserve space in the source container
  const sourceDummy = destDummy.cloneNode(true);
  destDummy.style.width = 0;
  targetContainer.insertBefore(destDummy, targetContainer.firstChild);
  sourceContainer.insertBefore(sourceDummy, box);
  let rect = box.getBoundingClientRect();
  box.classList.toggle('abs');
  box.style.left = rect.left-rect.width/2+'px';
  box.style.top = rect.top+'px';

  // Animate the box to the target container
  requestAnimationFrame(() => {
    sourceDummy.style.width = 0;
    destDummy.style.removeProperty('width');
    box.style.transition = `transform ${transDur}ms ease-in-out`;
    box.style.transform = `translate(${destDummy.offsetLeft - box.offsetLeft - box.offsetWidth/2}px, ${destDummy.offsetTop - box.offsetTop}px)`;
    setTimeout(() => {
      // Remove the placeholder and move the box to the target container
      sourceContainer.removeChild(box);
      sourceContainer.removeChild(sourceDummy);
      targetContainer.removeChild(destDummy);
      targetContainer.insertBefore(box, targetContainer.firstChild);
      box.style.removeProperty('transition');
      box.style.removeProperty('transform');
      box.style.removeProperty('left');
      box.style.removeProperty('top');
      box.classList.toggle('abs');
    }, transDur); // Wait for transition to complete before removing placeholder and moving box
  });
}

// Initialize the containers with color boxes
const containerA = document.getElementById('containerA');
const containerB = document.getElementById('containerB');

for (let i = 0; i < 3; i++) {
  createColorBox(containerA);
}
for (let i = 0; i < 3; i++) {
  createColorBox(containerB);
}

Kind of a lengthy approach but this uses placeholders at the source and destination that changes their widths and the transition affect will make the rest of the flexbox items in the containers to adjust smoothly.

Note: Change the value of transDur variable to change the transition duration.

Semang answered 7/3 at 8:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.