Is it possible to animate Flexbox inserts & removes?
CssCss TransitionsCss AnimationsFlexboxCss 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:
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.
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>