main.content
  ul.items
    - for (let i = 1; i <= 24; i++)
      li.item(data-product-id= i)
        button.btn-item(data-product-id= i)= i
        
aside.cart
  .btn-cart-wrapper
    button.btn-cart
      svg.icon(aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="192" height="192" fill="currentcolor" viewBox="0 0 256 256")
        rect(width="256" height="256" fill="none")
        path(d="M184,184H69.8L41.9,30.6A8,8,0,0,0,34.1,24H16" fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16")
        circle(cx="80" cy="204" r="20" fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16")
        circle(cx="184" cy="204" r="20" fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16")
        path(d="M62.5,144H188.1a15.9,15.9,0,0,0,15.7-13.1L216,64H48" fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16")
    .count
  .items-wrapper
    .empty-text Your cart is empty
    .items
View Compiled
* {
  box-sizing: border-box;
}

body {
  --color-primary: hotpink;
  --color-secondary: white;
  --color-tertiary: dodgerblue;
  --padding: clamp(1rem, 2vw, 2rem);
  --radius: 0.25rem;
  --shadow: 0 1rem 2rem hsla(0 0% 0% / 0.2);

  margin: 0;
  font-family: monospace, monospace;
}


/* ITEMS */

.items:not(:empty) {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--padding);
  padding: calc(var(--padding) * 2);
  grid-template-columns: var(--columns, 1fr);
}

@media (min-width: 350px) {
  .items {
    --columns: repeat(auto-fit, minmax(14rem, 1fr));
  }
}

.item {
  position: relative;
  display: grid;
  aspect-ratio: 1;
  border-radius: var(--radius);
}

.item.in-cart {
  color: var(--color-primary);
  border: 2px dashed currentcolor;
  z-index: 1;
}

.item.active {
  z-index: 2;
}



/* BUTTONS */

[class*="btn"] {
  all: unset;
}

[class*="btn"]:active {
  transform: translateY(2px);
}

[class*="btn"]:focus-visible {
  --size: 3px;
  outline: var(--size) solid var(--color-tertiary);
  outline-offset: var(--size);
}

.btn-item {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  user-select: none;
  font-size: 2rem;
  color: var(--color-secondary);
  background-color: var(--color-primary);
  border-radius: var(--radius);
}

.btn-cart {
  display: flex;
  align-items: center;
  padding: 1rem;
  font-size: 1.5rem;
  background: var(--color-secondary);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.btn-cart svg {
  width: 1.5em;
  height: 1.5em;
}




/* CART */

.cart {
  display: grid;
  place-items: end;
  position: fixed;
  bottom: var(--padding);
  right: var(--padding);
  width: 100%;
  min-width: 0;
  pointer-events: none;
  z-index: 3;
  transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.cart:not(.open) {
  transform: translateY(calc(100% + var(--padding)));
}

.cart:not(.open) .items-wrapper {
  visibility: hidden;
  transition-delay: 300ms;
}

.cart .items-wrapper {
  display: grid;
  overflow: auto;
  width: calc(100% - var(--padding) * 2);
  max-height: 75vh;
  max-width: 350px;
  background-color: var(--color-secondary);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  pointer-events: initial;
  transition: visibility 0s;
}

.cart .items {
  --columns: repeat(auto-fill, minmax(3rem, 1fr));
  --padding: 0.5rem;
}

.cart .count {
  --size: 1.75em;
  position: absolute;
  top: -0.65em;
  right: -0.75em;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-secondary);
  background-color: var(--color-primary);
  font-size: 0.9rem;
  letter-spacing: -0.08em;
  width: var(--size);
  height: var(--size);
  border-radius: 50%;
}

.cart .count:empty {
  display: none;
}

.cart .items .btn-item {
  font-size: 1rem;
}

.cart .empty-text {
  grid-column: 1 / -1;
  text-align: center;
  padding: 1rem;
}

.btn-cart-wrapper {
  position: absolute;
  bottom: calc(100% + var(--padding));
  pointer-events: initial;
  z-index: 1;
}

.btn-cart-wrapper .btn-item {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

@media (prefers-reduced-motion) {
  .cart {
    transition-duration: 0s;
  }
  
  .cart:not(.open) .items-wrapper {
    transition-delay: 0s;
  }
}
gsap.registerPlugin(Flip, CustomWiggle);

CustomWiggle.create("cartButtonWiggle", { wiggles: 8, type: "easeOut" });

const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const pageItems = document.querySelector(".content .items");
const cart = document.querySelector(".cart");
const cartBtnWrapper = cart.querySelector(".btn-cart-wrapper");
const cartBtn = cart.querySelector(".btn-cart");
const cartCount = cart.querySelector(".count");
const cartItems = cart.querySelector(".items");
const cartEmptyText = cart.querySelector(".empty-text");

const setInCartClass = (item, inCart) => item.parentNode.classList.toggle('in-cart', inCart);
const setActiveItemClass = (item, isActive) => item.parentNode.classList.toggle('active', isActive);

const updateCart = (item) => {
  const hasItems = cartItems.children.length > 0;
  
  cartCount.innerText = hasItems ? cartItems.children.length : null;
  cartEmptyText.hidden = hasItems;
  cartItems.hidden = !hasItems;
}

const cartBtnAnimation = () => {
  gsap.timeline()
    .fromTo(cartBtn, { yPercent: 0, rotation: 0 },
    {
      duration: 0.9,
      ease: "cartButtonWiggle",
      yPercent: 20,
      rotation: -5,
      clearProps: 'all'
    })
    .fromTo(cartCount, { rotation: 0 }, {
      duration: 1.3,
      ease: "power4.out",
      rotation: 720,
      y: -30,
    }, "<")
    .to(cartCount, {
      duration: 0.8,
      ease: "elastic.out(1, 0.3)",
      y: 0,
      clearProps: 'all'
    }, "-=0.6");
}

const addToCart = (item) => {
  const state = Flip.getState(item);

  setInCartClass(item, true);
  setActiveItemClass(item, true);
  cartBtnWrapper.appendChild(item);

  Flip.from(state, {
    duration: reducedMotion ? 0 : 0.5,
    ease: "back.in(0.8)",
    onComplete: () => {
      setActiveItemClass(item, false);
      cartItems.appendChild(item);
      
      gsap.fromTo(item,
        { y: -12 },
        { 
          y: 0, 
          duration: reducedMotion ? 0 : 1,
          ease: "elastic.out(1, 0.3)"
        });
      
      updateCart(item);
      !reducedMotion && cartBtnAnimation();
    }
  });
};

const removeFromCart = (item) => {
  let state = Flip.getState(item);
    
  document.querySelector(`[data-product-id="${item.dataset.productId}"]`).appendChild(item);
  updateCart(item);
  setActiveItemClass(item, true);

  Flip.from(state, {
    duration: reducedMotion ? 0 : 0.5,
    ease: "power4.out",
    onComplete: () => {
      setActiveItemClass(item, false);
      setInCartClass(item, false);
    }
  });
};

pageItems.addEventListener("click", (e) => {
  if (e.target.classList.contains("btn-item")) {
    addToCart(e.target);
  }
});

cartItems.addEventListener("click", (e) => {
  if (e.target.classList.contains("btn-item")) {
    removeFromCart(e.target);
  }
});

cartBtn.addEventListener("click", () => cart.classList.toggle("open"));
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/gsap-latest-beta.min.js
  2. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/CustomEase3.min.js
  3. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/CustomBounce3.min.js
  4. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/CustomWiggle3.min.js
  5. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/InertiaPlugin.min.js
  6. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/Draggable3.min.js
  7. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/EasePack3.min.js
  8. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/Flip.min.js