<div id="root" class="panel center"></div>
.button {
  user-select: none;
}

.app {
  padding: 10px;
}

.box {
  margin: 10px 10px 0 0;
  user-select: none;
}

.box.exiting,
.box.exited {
  display: none;
}

.boxes {  
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
}

button {
  margin: 1rem 0.5rem;
}
const { useState, useLayoutEffect, useRef } = React;

let count = 0;

gsap.registerPlugin(Flip);
gsap.config({ trialWarn: false });

const wrapColor = gsap.utils.wrap(["blue", "red", "purple", "orange"])

function createItem() {
  return { id: ++count, color: wrapColor(count), status: "entered" }
}

function App() {    
  const el = useRef();
  const q = gsap.utils.selector(el);
    
  const [layout, setLayout] = useState(() => {    
    return {
      items: [
        createItem(),
        createItem(),
        createItem(),
        createItem()
      ].reverse()
    };
  })

  useLayoutEffect(() => {
    
    if (!layout.state) return;
    
    // get the items that are exiting in this batch
    const exiting = layout.items.filter(item => item.status === "exiting");
    
    // Flip.from returns a timeline
    const timeline = Flip.from(layout.state, {
      absolute: true, 
      ease: "power1.inOut",
      targets: q(".box, .button"),
      scale: true,
      simple: true,
      onEnter: elements => {
        return gsap.fromTo(elements, { 
          opacity: 0,
          scale: 0
        }, { 
          opacity: 1,
          scale: 1,
          delay: 0.2,
          duration: 0.3
        });
      },
      onLeave: elements => {
        return gsap.to(elements, { 
          opacity: 0, 
          scale: 0 
        });
      }
    });
    
    // remove the exiting items from the DOM after the animation is done
    timeline.add(() => removeItems(exiting));
    
  }, [layout]);
  
  const removeItems = (exitingItems) => {
    
    if (!exitingItems.length) return;
    
    setLayout(prev => ({
      state: Flip.getState(q(".box, .button")),
      items: prev.items.filter(item => !exitingItems.includes(item))
    }));
  };
    
  const addItem = () => {    
    setLayout({
      state: Flip.getState(q(".box, .button")),
      items: [createItem(), ...layout.items]
    });
  };
  
  const shuffle = () => {    
    setLayout({
      state: Flip.getState(q(".box, .button")),
      items: [...gsap.utils.shuffle(layout.items)]
    });
  };
    
  const remove = (item) => {    
    
    // set the item as exiting which will add a CSS class for display: none;
    item.status = "exiting";    
    
    setLayout({
      ...layout,
      state: Flip.getState(q(".box, .button")),
    });  
  };
  
  return (
    <div className="app text-center" ref={el}>
      <div>
        <button className="button" onClick={addItem}>Add Box</button>
        <button className="button" onClick={shuffle}>Shuffle</button>
      </div>      
      <div className="boxes">
        {layout.items.map((item) => (
          <div      
            id={`box-${item.id}`} 
            key={item.id}
            className={`box ${item.color} ${item.status}`} 
            onClick={() => remove(item)} 
          >
            Click Me
          </div>
        ))}
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector("#root"));
View Compiled

External CSS

  1. https://codepen.io/GreenSock/pen/gOWxmWG.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js
  3. https://assets.codepen.io/16327/Flip.min.js
  4. https://unpkg.co/gsap@3/dist/gsap.min.js