<header view-transition-name="buttons-header">
  <button id="add" aria-label="Add List Item in Random Position" view-transition-name="add-button">+</button>
  <button id="remove" aria-label="Remove List Item from Random Position" view-transition-name="remove-button">-</button>
</header>

<ul id="list">
  <!-- these all need unique `view-transition-name`s because they all need to move independantly. They may move or not move depending on list position.-->
  <li style="view-transition-name: item-0">Apple</li>
  <li style="view-transition-name: item-1">Banana</li>
  <li style="view-transition-name: item-2">Guanabana</li>
  <li style="view-transition-name: item-3">Star Fruit</li>
  <li style="view-transition-name: item-4">Dragonfruit</li>
</ul>
ul {
  display: grid;
  gap: 0.5rem;
  padding: 0;
  width: max-content;
}

ul > li {
  background: white;
  padding: 0.5rem 1rem;
  border-radius: 0.3vw;
  overflow: hidden;
  contain: layout;
}

ul > li.incoming {
  animation: 0.6s incoming both;
}
::view-transition-old(outgoing) {
  animation: 1s outgoing both;
}

@keyframes outgoing {
  0% {
    translate: 0 0;
    scale: 1;
    opacity: 1;
  }
  100% {
    translate: 100px -50px;
    scale: 1.2;
    opacity: 0;
  }
}
@keyframes incoming {
  0% {
    scale: 1.6;
    opacity: 0;
    translate: -100px 0;
  }
  100% {
    scale: 1;
    opacity: 1;
    translate: 0 0;
  }
}

body {
  font: 100%/1.4 system-ui, sans-serif;
  background: #455a64;
  height: 100vh;
  margin: 0;
  display: grid;
  place-content: center;
}
header {
  display: flex;
  gap: 1rem;
}
header > button {
  flex: 1;
  background: #90a4ae;
  color: white;
  border: 0;
  border-radius: 100px;
  line-height: unset;
}
header > button:hover,
header > button:focus-visible {
  background: #b0bec5;
}
button {
  font-family: inherit;
}
import { faker } from "https://esm.sh/@faker-js/[email protected]";

const list = document.querySelector("#list");
const addButton = document.querySelector("#add");
const removeButton = document.querySelector("#remove");

let startIndex = 5;

addButton.addEventListener("click", () => {
  // Fallback for browsers without `startViewTransition`
  if (!document.startViewTransition) {
    addItem();
    return;
  }

  // FLIP!
  const transition = document.startViewTransition(() => {
    addItem();
  });
});

removeButton.addEventListener("click", () => {
  const randomListItem = getRandomItem();

  randomListItem.classList.remove("incoming");
  randomListItem.style.viewTransitionName = "outgoing";

  if (!document.startViewTransition) {
    randomListItem.remove();
    return;
  }
  const transition = document.startViewTransition(() => {
    randomListItem.remove();
  });
});

function addItem() {
  const newListItem = document.createElement("li");
  newListItem.innerHTML = faker.random.words();
  // new list items need to have a unique `view-transition-name` because they need to move independantly, like all other list items. They may move or not move depending on list position.
  newListItem.style.viewTransitionName = `item-${++startIndex}`;
  newListItem.classList.add("incoming");
  const numberOfListItems = list.querySelectorAll(`:scope > li`).length;
  const randomPosition = getRandomInt(0, numberOfListItems);
  if (randomPosition === 0) {
    list.insertAdjacentElement("afterBegin", newListItem);
  } else {
    const randomListItem = list.querySelector(
      `:scope > :nth-child(${randomPosition})`
    );
    randomListItem.insertAdjacentElement("afterEnd", newListItem);
  }
}

function getRandomItem() {
  const numberOfListItems = list.querySelectorAll(`:scope > li`).length;
  const randomPosition = getRandomInt(1, numberOfListItems);
  return list.querySelector(`:scope > :nth-child(${randomPosition})`);
}

function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.