Дергание нижних карточек при раскрытии (перемещение на уровень выше)

Я реализовал список карточек, при помощи интернета, которые раскрываются при клике. Если места для раскрытия недостаточно, карточка перемещается на уровень выше с помощью transform и JS.

Проблема:
Когда нижняя карточка раскрывается, карточки "дергаются" — сначала опускаются вниз, а затем возвращаются на нужную высоту. Это заметно на глаз и выглядит неаккуратно.

В чем может быть проблема?

document.querySelectorAll(".card").forEach(card => {
  card.addEventListener("click", function() {
    const cardItem = this.parentElement;
    const grid = cardItem.parentElement;

    const itemHeight = parseFloat(window.getComputedStyle(cardItem).height);
    const gridGap = parseFloat(window.getComputedStyle(grid).gap);

    const expandedHeight = itemHeight * 2 + gridGap;


    if (this.classList.contains("expanded")) {
      this.classList.remove("expanded", "expanded-up");
      this.style.height = "";
      this.style.transform = "";
      return;
    }


    document.querySelectorAll(".card.expanded").forEach(c => {
      c.classList.remove("expanded", "expanded-up");
      c.style.height = "";
      c.style.transform = "";
    });


    const cardRect = cardItem.getBoundingClientRect();

    const gridRect = grid.getBoundingClientRect();
    const spaceBelow = gridRect.bottom - cardRect.bottom;
    const neededSpace = itemHeight + gridGap;

    if (spaceBelow < neededSpace) {

      this.classList.add("expanded", "expanded-up");
      this.style.height = `${expandedHeight}px`;
      this.style.transform = `translateY(calc(-50% - ${gridGap / 2}px))`;
    } else {
      this.classList.add("expanded");
      this.style.height = `${expandedHeight}px`;
    }
  });
});
* {
  box-sizing: border-box;
}

html,
body {
  box-sizing: border-box;
}

.cards {
  display: grid;
  grid-template-columns: repeat(3, 300px);
  grid-auto-rows: 400px;
  gap: 20px;
  list-style: none;
  padding: 0;
  margin: 0;
  position: relative;
}

.card-item {
  position: relative;
  height: 400px;
}

.card {
  position: absolute;
  width: 100%;
  height: 400px;
  background: #f0f0f0;
  border: 1px solid #ccc;
  border-radius: 8px;
  padding: 20px;
  transition: all 0.3s ease;
  cursor: pointer;
  overflow: hidden;
  z-index: 1;
}

.card.expanded {
  z-index: 100;
  background: #fff;
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
<ul class="cards">
  <li class="card-item">
    <div class="card">Карточка 1</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 2</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 3</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 4</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 5</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 6</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 7</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 8</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 9</div>
  </li>
</ul>


Ответы (1 шт):

Автор решения: Grundy

Проблема в вычислении внутри transform. Так как конечная высота известна, нет смысла в использовании дополнительного calc.

Конечное смещение можно вычислить сразу: -expandedHeight/2 - gridGap/2

document.querySelectorAll(".card").forEach(card => {
  card.addEventListener("click", function() {
    const cardItem = this.parentElement;
    const grid = cardItem.parentElement;

    const itemHeight = parseFloat(window.getComputedStyle(cardItem).height);
    const gridGap = parseFloat(window.getComputedStyle(grid).gap);

    const expandedHeight = itemHeight * 2 + gridGap;


    if (this.classList.contains("expanded")) {
      this.classList.remove("expanded", "expanded-up");
      this.style.height = "";
      this.style.transform = "";
      return;
    }


    document.querySelectorAll(".card.expanded").forEach(c => {
      c.classList.remove("expanded", "expanded-up");
      c.style.height = "";
      c.style.transform = "";
    });


    const cardRect = cardItem.getBoundingClientRect();

    const gridRect = grid.getBoundingClientRect();
    const spaceBelow = gridRect.bottom - cardRect.bottom;
    const neededSpace = itemHeight + gridGap;

    if (spaceBelow < neededSpace) {

      this.classList.add("expanded", "expanded-up");
      this.style.height = `${expandedHeight}px`;
      this.style.transform = `translateY(${-expandedHeight/2 - gridGap/2 }px)`;
    } else {
      this.classList.add("expanded");
      this.style.height = `${expandedHeight}px`;
    }
  });
});
* {
  box-sizing: border-box;
}

html,
body {
  box-sizing: border-box;
}

.cards {
  display: grid;
  grid-template-columns: repeat(3, 300px);
  grid-auto-rows: 400px;
  gap: 20px;
  list-style: none;
  padding: 0;
  margin: 0;
  position: relative;
}

.card-item {
  position: relative;
  height: 400px;
}

.card {
  position: absolute;
  width: 100%;
  height: 400px;
  background: #f0f0f0;
  border: 1px solid #ccc;
  border-radius: 8px;
  padding: 20px;
  transition: all 0.3s ease;
  cursor: pointer;
  overflow: hidden;
  z-index: 1;
}

.card.expanded {
  z-index: 100;
  background: #fff;
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
<ul class="cards">
  <li class="card-item">
    <div class="card">Карточка 1</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 2</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 3</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 4</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 5</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 6</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 7</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 8</div>
  </li>
  <li class="card-item">
    <div class="card">Карточка 9</div>
  </li>
</ul>

→ Ссылка