анимация css перестроение карточек по выбранной категории

подскажите, как реализовать. есть начало https://codepen.io/YuliyaVoronovich/pen/VwNapeb нужно чтобы при клике на категорию карточки нужной категории плавно перемещались. сейчас они просто исчезают и появляются и остаются "дыры" между. нужно как-то пересчитывать позицию и я не понимаю как это сделать

const tabs = document.querySelectorAll('.tabs button');
const cardsContainer = document.querySelector('.cards');

const loadCards = (category) => {
    const cards = Array.from(cardsContainer.children);

    // Сначала скрываем карточки
    cards.forEach(card => {
        card.classList.add('hide'); // Добавляем класс hide для анимации скрытия 
        });

    // Ждем завершения анимации скрытия 
    setTimeout(() => {
        let matchingCards;

        // Определяем, какие карточки показывать 
        if (category === 'all') {
            matchingCards = cards; // Показываем все карточки 
            } else {
            matchingCards = cards.filter(card => card.getAttribute('data-categories').includes(category));
        }

        // Убираем класс hide у соответствующих карточек с задержкой 
        matchingCards.forEach((card, index) => {
            setTimeout(() => {
                card.classList.remove('hide'); // Убираем класс hide для появления
                card.style.opacity = '1'; // Устанавливаем opacity в 1 для анимации
                card.style.transform = 'translateY(0)'; // Возвращаем карточку в исходное положение
            }, index * 100); // Задержка в 100 мс между карточками
        });
    }, 400); // Длительность анимации скрытия
};

// Обработчик для табов
tabs.forEach(tab => {
    tab.addEventListener('click', () => {
        const category = tab.getAttribute('data-category');
        loadCards(category);
    });
});

.cards {
    display: flex;
    flex-wrap: wrap;
    gap: 16px; /* Промежуток между карточками */
}

.cards-child {
    flex: 1 1 calc(33.333% - 16px); /* Примерный размер карточек (3 в ряд) */
    box-sizing: border-box; /* Включаем бордеры в размеры */
    border: 2px solid #ccc; /* Бордеры для карточек */
    border-radius: 8px; /* Закругление углов */
    padding: 16px; /* Отступ внутри карточек */
    transition: opacity 0.4s ease, transform 0.4s ease; /* Плавный переход для карточек */
    opacity: 1; /* Начальное состояние (видимо) */
    transform: translateY(0); /* Исходная позиция */
}

.cards-child.hide {
    opacity: 0; /* Полностью невидим */
    visibility: hidden; /* Скрываем элемент, но он занимает место */
    transform: translateY(-20px); /* Сдвигаем карточку вверх для анимации */
}

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

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

Используйте display, а не visibility и добавь анимацию. В итоге твой css файл должен быть примерно такой

.cards {
    display: flex;
    flex-wrap: wrap;
    gap: 16px; /* Промежуток между карточками */
}

@keyframes fadeIn {
    0% {
        opacity: 0;
        transform: translateY(20px); /* Начальное положение снизу */
    }
    100% {
        opacity: 1;
        transform: translateY(0); /* Конечное положение */
    }
}

.cards-child {
    flex: 1 1 calc(33.333% - 16px);
    box-sizing: border-box;
    border: 2px solid #ccc;
    border-radius: 8px;
    padding: 16px;
    transition: opacity 0.4s ease, transform 0.4s ease;
    opacity: 0; /* Начальное состояние (невидимо) */
    transform: translateY(20px); /* Исходная позиция (снизу) */
    animation: fadeIn 0.6s forwards; /* Применить анимацию появления */
}

.cards-child.hide {
    display: none; /* Скрываем элемент, но он занимает место */
    transform: translateY(-20px); /* Сдвигаем карточку вверх для анимации */
}
→ Ссылка
Автор решения: Andrei Fedorov

Вот как я бы решил эту задачу. Тут есть ряд изображений, которые в обычном случае располагались бы один под другим. Но мы контролируем их позиции с помощью transform: translate. У нас есть ширина и высота ячейки и с поправкой на gap, можно задать координаты изображений с помощью коэффициентов. Например, второе изображение нам нужно сместить на одну позицию вправо (коэф. = 1) и на одну позицию наверх (коэф. = -1). Если коэффициенты не заданы undefined, то показывать изображение не нужно. Ссылка на CodePen

UP: Поправил отсутствие анимации между odd и even

UP2: Добавил расчет высоты обертки .list

function show(coords) {
  const slidesCount = coords.filter((element) => element !== undefined).length;
  const rows = Math.floor((slidesCount - 1) / 3) + 1;
  console.log(slidesCount, rows);
  document.querySelector(".list").style.maxHeight = `calc(${rows} * var(--item-height) - 2em)`;
  const refCoords = [
    [0, 0],
    [1, -1],
    [2, -2],
    [0, -2],
    [1, -3]
  ];
  let items = document.querySelectorAll(".item");
  for (i = 0; i < items.length; i++) {
    items[i].style.opacity = coords[i] ? 1 : 0;
    const [x, y] = coords[i] || refCoords[i];
    items[
      i
    ].style.transform = `translate(calc(${x} * var(--item-width)), calc(${y} * var(--item-height)))`;
  }
}

addEventListener("click", (e) => {
  let b = e.target;
  if (b.classList.contains("filter") && b.ariaPressed !== "true") {
    let a = document.querySelector(".filter[aria-pressed=true]");
    [b.ariaPressed, a.ariaPressed] = [a.ariaPressed, b.ariaPressed];
  }
});
:root {
  --item-width: calc(100% + 2em);
  --item-height: calc(300px + 2em);
}

.filter-set {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.4em;
  padding: 2em 0;
}

.filter {
  background-color: #088;
  color: white;
  font-weight: 600;
  padding: 0.8em 1.6em;
  border: none;
  border-radius: 0.3em;
  transition: 0.3s;
  box-shadow: 0 0 10px #0005;
}

.filter:hover {
  background-color: #059;
}

.filter[aria-pressed="true"] {
  background-color: #066;
}

.list {
  overflow: hidden;
  max-height: calc(2 * var(--item-height) - 2em);
}

.item {
  display: block;
  object-fit: cover;
  background-color: lightblue;
  width: calc(var(--item-width) / 3 - 2em);
  height: calc(var(--item-height) - 2em);
  margin-bottom: 2em;
  transition: 0.3s;
  opacity: 1;
}


/* 0 0 */

.item:nth-child(1) {
  transform: translate( calc(0 * var(--item-width)), calc(0 * var(--item-height)));
}


/* 1 -1 */

.item:nth-child(2) {
  transform: translate( calc(1 * var(--item-width)), calc(-1 * var(--item-height)));
}


/* 2 -2 */

.item:nth-child(3) {
  transform: translate( calc(2 * var(--item-width)), calc(-2 * var(--item-height)));
}


/* 0 -2 */

.item:nth-child(4) {
  transform: translate( calc(0 * var(--item-width)), calc(-2 * var(--item-height)));
}


/* 1 -3 */

.item:nth-child(5) {
  transform: translate( calc(1 * var(--item-width)), calc(-3 * var(--item-height)));
}

body {
  font: sans-serif;
  padding: 0 2em;
}
<div class="filter-set">
  <button class="filter" aria-pressed="true" onclick="show([[0,0],[1,-1],[2,-2],[0,-2],[1,-3]]);">All</button>
  <button class="filter" onclick="show([[0,0],undefined,[1,-2],undefined,[2,-4]]);">Odd</button>
  <button class="filter" onclick="show([undefined,[0,-1],undefined,[1,-3],undefined]);">Even</button>
  <button class="filter" onclick="show([undefined,undefined,undefined,[0,-3],[1,-4]]);">The last two</button>
</div>

<div class="list">
  <img class="item" src="https://picsum.photos/id/209/400/300">
  <img class="item" src="https://picsum.photos/id/308/400/300">
  <img class="item" src="https://picsum.photos/id/407/400/300">
  <img class="item" src="https://picsum.photos/id/506/400/300">
  <img class="item" src="https://picsum.photos/id/605/400/300">
</div>

→ Ссылка