Как сверстать каталог интернет-магазина

Эта статья основана на одном из практических заданий verstaem.online.

Вы можете читать материал как обычное руководство или сначала попробовать выполнить задание самостоятельно, а затем сравнить свое решение с подробным разбором.

Перейти к заданию

В этом задании мы сверстаем страницу каталога товаров, похожую на те, что используются в интернет-магазинах. Здесь есть сразу несколько интересных элементов: боковая панель с фильтрами, адаптивная сетка товаров, карточки с ценами и скидками, а также мобильная версия, в которой фильтры открываются по кнопке.

Создаем HTML-разметку

Сначала создадим базовую HTML-разметку с помощью Emmet. Для этого достаточно ввести ! и нажать Tab. Обратите внимание на атрибут lang. Поскольку основной контент страницы написан на русском языке, здесь используем значение ru.

<!doctype html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Каталог товаров</title>
  </head>
  <body>
  </body>
</html>

Весь контент страницы представим как <div>, внутри него контейнер-обертка, а еще внутри — две части. Боковую панель разметим через <aside>, а основную часть — через <div>.

<div class="content">
  <div class="container grid">
    <aside class="content__aside" data-aside>
    </aside>
    <div class="content__main">
    </div>
  </div>
</div>

Обратите внимание, что я добавил data-aside к боковой панели. Это дата-атрибут, и после data- вы можете задать все, что вам угодно. В данном случае этот атрибут (и еще несколько впереди) мы используем для связи с js. Здесь мы будем писать небольшой скрипт, и нам нужно связать html и js.

Идем дальше, и делаем боковую панель. здесь будет заголовок, кнопка закрытия (которую мы скроем на десктопе), а также набор списков. Каждый блок фильтров — это заголовок + список. А сами фильтры — кнопки. Все довольно просто.

<aside class="content__aside" data-aside>
  <h2 class="content__subtitle">Фильтры</h2>
  <button class="content__close" aria-label="Закрыть фильтр" data-close>
    <svg
      width="20"
      height="20"
      viewBox="0 0 20 20"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g clip-path="url(#clip0_29001_517)">
        <path
          d="M13.9225 7.25583L11.1783 10L13.9225 12.7442L12.7442 13.9225L10 11.1783L7.25583 13.9225L6.0775 12.7442L8.82167 10L6.0775 7.25583L7.25583 6.0775L10 8.82167L12.7442 6.0775L13.9225 7.25583ZM20 10C20 15.5142 15.5142 20 10 20C4.48583 20 0 15.5142 0 10C0 4.48583 4.48583 0 10 0C15.5142 0 20 4.48583 20 10ZM18.3333 10C18.3333 5.405 14.595 1.66667 10 1.66667C5.405 1.66667 1.66667 5.405 1.66667 10C1.66667 14.595 5.405 18.3333 10 18.3333C14.595 18.3333 18.3333 14.595 18.3333 10Z"
          fill="#17212B"
        />
      </g>
      <defs>
        <clipPath id="clip0_29001_517">
          <rect width="20" height="20" fill="white" />
        </clipPath>
      </defs>
    </svg>
  </button>
  <div class="content__inner">
    <h3 class="content__caption">Категория</h3>
    <ul class="content__list">
      <li class="content__item">
        <button class="content__btn">
          Ноутбуки
          <span class="content__quantity">28</span>
        </button>
      </li>
      <li class="content__item">
        <button class="content__btn">
          Мониторы
          <span class="content__quantity">16</span>
        </button>
      </li>
      <li class="content__item">
        <button class="content__btn">
          Видеокарты
          <span class="content__quantity">11</span>
        </button>
      </li>
      <li class="content__item">
        <button class="content__btn">
          Наушники
          <span class="content__quantity">25</span>
        </button>
      </li>
      <li class="content__item">
        <button class="content__btn">
          Периферия
          <span class="content__quantity">42</span>
        </button>
      </li>
    </ul>
  </div>
  <div class="content__inner">
    <h3 class="content__caption">Цена</h3>
    <ul class="content__list">
      <li class="content__item">
        <button class="content__btn">до 10 000 ₽</button>
      </li>
      <li class="content__item">
        <button class="content__btn">10–30 тыс.</button>
      </li>
      <li class="content__item">
        <button class="content__btn">30–70 тыс.</button>
      </li>
      <li class="content__item">
        <button class="content__btn">70–150 тыс.</button>
      </li>
      <li class="content__item">
        <button class="content__btn">150+ тыс.</button>
      </li>
    </ul>
  </div>
  <div class="content__inner">
    <h3 class="content__caption">Бренды</h3>
    <ul class="content__list">
      <li class="content__item">
        <button class="content__btn">Apple</button>
      </li>
      <li class="content__item">
        <button class="content__btn">Sony</button>
      </li>
      <li class="content__item">
        <button class="content__btn">ASUS</button>
      </li>
      <li class="content__item">
        <button class="content__btn">Logitech</button>
      </li>
      <li class="content__item">
        <button class="content__btn">Samsung</button>
      </li>
      <li class="content__item">
        <button class="content__btn">MSI</button>
      </li>
    </ul>
  </div>
  <div class="content__inner">
    <h3 class="content__caption">Статус</h3>
    <ul class="content__list">
      <li class="content__item">
        <button class="content__btn">В наличии</button>
      </li>
      <li class="content__item">
        <button class="content__btn">Под заказ</button>
      </li>
      <li class="content__item">
        <button class="content__btn">Новинки</button>
      </li>
      <li class="content__item">
        <button class="content__btn">Распродажа</button>
      </li>
    </ul>
  </div>
</aside>

Обратите внимание, что кнопке мы задали data-close, чтобы тоже связать ее с js. Также стоит обратить внимание на количество товаров в первом блоке фильтров — их я сделал через <span>, так как нам пригодится элемент, чтобы оттолкнуться от текста слева.

Далее — основная часть. Тут мы сделаем два блока — верхний, где заголовок, количество товаров, а еще кнопка фильтров (которая будет не видна на десктопе), и нижний — список товаров.

<div class="content__top">
  <h1 class="content__title">Каталог товаров</h1>
  <div class="content__count">236 товаров</div>
  <button class="content__filter" data-filter>
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g clip-path="url(#clip0_29001_364)">
        <path
          d="M9.33324 16C9.18899 16 9.04864 15.9532 8.93324 15.8667L6.26657 13.8667C6.18377 13.8046 6.11657 13.724 6.07029 13.6315C6.024 13.5389 5.99991 13.4368 5.99991 13.3333V9.58667L1.32257 4.32467C0.990386 3.94992 0.773492 3.48721 0.697959 2.99216C0.622426 2.4971 0.691468 1.99077 0.896788 1.53401C1.10211 1.07725 1.43497 0.689506 1.85536 0.417377C2.27575 0.145249 2.76579 0.000316279 3.26657 0L12.7332 0C13.234 0.000587367 13.7239 0.14576 14.1441 0.418071C14.5643 0.690383 14.897 1.07825 15.1021 1.53506C15.3072 1.99187 15.376 2.49818 15.3003 2.99316C15.2246 3.48815 15.0075 3.95073 14.6752 4.32533L9.99991 9.58667V15.3333C9.99991 15.5101 9.92967 15.6797 9.80464 15.8047C9.67962 15.9298 9.51005 16 9.33324 16ZM7.33324 13L8.66657 14V9.33333C8.66671 9.1701 8.72672 9.0126 8.83524 8.89067L13.6806 3.43933C13.8421 3.25672 13.9476 3.03136 13.9842 2.7903C14.0208 2.54924 13.9871 2.30274 13.8871 2.08037C13.7871 1.85801 13.625 1.66924 13.4203 1.53673C13.2156 1.40421 12.9771 1.33359 12.7332 1.33333H3.26657C3.02287 1.3337 2.78445 1.40436 2.57991 1.53684C2.37536 1.66932 2.21338 1.858 2.11338 2.08023C2.01338 2.30247 1.97961 2.54884 2.01612 2.78979C2.05264 3.03074 2.15788 3.25604 2.31924 3.43867L7.16524 8.89067C7.27352 9.0127 7.33329 9.17019 7.33324 9.33333V13Z"
        />
      </g>
      <defs>
        <clipPath id="clip0_29001_364">
          <rect width="16" height="16" />
        </clipPath>
      </defs>
    </svg>
    Фильтры
  </button>
</div>

Собственно, здесь снова обращу внимание на то, что мы добавили data-filter для кнопки.

Ну, а список товаров — это простая история. Делаем список, внутри каждого элемента списка — <article>, идеально подходящий под независимые товары, а внутри — элементы. Делай бейджик через <span> или <div> — ваше право. Я использовал первый вариант, потому что текст там минимальный, и делать прям блочный элемент смысла нет. Не забудьте также сделать ссылку внутри заголовка, иначе карточка будет некликабельна.

<ul class="content__products">
  <li class="content__products-item">
    <article class="product">
      <span class="product__badge">-20%</span>
      <img src="img/1.png" alt="LG UltraGear" class="product__pic" />
      <h3 class="product__title">
        <a href="#" class="product__link"> LG UltraGear </a>
      </h3>
      <p class="product__descr">Nano IPS, HDR, 180 Гц, USB‑C.</p>
      <div class="product__price">39 990 ₽</div>
      <button class="product__btn">Купить</button>
    </article>
  </li>
  <li class="content__products-item">
    <article class="product">
      <span class="product__badge">Новинка</span>
      <img src="img/2.png" alt="MX Keys S" class="product__pic" />
      <h3 class="product__title">
        <a href="#" class="product__link"> MX Keys S </a>
      </h3>
      <p class="product__descr">Клавиатура</p>
      <div class="product__price">10 990 ₽</div>
      <button class="product__btn">Купить</button>
    </article>
  </li>
  <li class="content__products-item">
    <article class="product">
      <span class="product__badge">-20%</span>
      <img src="img/3.png" alt="RTX 6080" class="product__pic" />
      <h3 class="product__title">
        <a href="#" class="product__link"> RTX 6080 </a>
      </h3>
      <p class="product__descr">24 ГБ GDDR7, DLSS, Ray Tracing.</p>
      <div class="product__price">199 990 ₽</div>
      <button class="product__btn">Купить</button>
    </article>
  </li>
  <li class="content__products-item">
    <article class="product">
      <span class="product__badge">-20%</span>
      <img src="img/4.png" alt="Web‑камера 4K" class="product__pic" />
      <h3 class="product__title">
        <a href="#" class="product__link"> Web‑камера 4K </a>
      </h3>
      <p class="product__descr">4K</p>
      <div class="product__price">13 490 ₽</div>
      <button class="product__btn">Купить</button>
    </article>
  </li>
  <li class="content__products-item">
    <article class="product">
      <img
        src="img/5.png"
        alt="Logitech G Pro X"
        class="product__pic"
      />
      <h3 class="product__title">
        <a href="#" class="product__link"> Logitech G Pro X </a>
      </h3>
      <p class="product__descr">Мышь</p>
      <div class="product__price">11 990 ₽</div>
      <button class="product__btn">Купить</button>
    </article>
  </li>
</ul>

Добавляем стили

Подключим файл стилей через <link> и зададим несколько базовых правил. Для этого задания достаточно переопределить box-sizing, сбросить отступ у <body> и подключить шрифт Inter, который используется в макете.

* {
  box-sizing: border-box;
}

@font-face {
  font-family: "Inter";
  src: url("../fonts/InterRegular.woff2");
  font-display: swap;
  font-weight: 400;
}

@font-face {
  font-family: "Inter";
  src: url("../fonts/InterBold.woff2");
  font-display: swap;
  font-weight: 700;
}

body {
  margin: 0;
  padding: 24px 0;
  background: #eef2f7;
  font-family: "Inter", sans-serif;
}

.stop-scroll {
  overflow: hidden;
}

.container {
  margin: 0 auto;
  padding: 0 15px;
  max-width: 1730px;
}

.grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  align-items: start;
  gap: 20px;
}

Помимо стандартных подключений шрифтов и сбросов, мы также добавляем класс .stop-scroll, потому что нам потребуется заблокировать скролл основной страницы, пока открыты фильтры. Ну и конечно, обращу ваше внимание на класс .grid, где мы создаем сетку из 12-ти колонок, как в макете.

Далее стили для левой колонки. Тут все вполне просто — задаем размер через grid-column, добавляем Flexbox для выравнивания всех внутренних элементов.

.content__aside {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 16px;
  grid-column: 3 span;
  border-radius: 24px;
  padding: 24px;
  box-shadow: 0 15px 50px 0 rgba(0, 0, 0, 0.08);
  background: #fff;
}

Далее добавим стили для кнопки закрытия. Да, мы можем уже сейчас разместить ее как нам нужно, и добавить display: none, чтобы она скрылась. Я решил использовать position: absolute, чтобы кнопка никак не мешала потоку элементов. Ее задача — висеть справа и ждать, пока на нее кликнут.

Далее поработаем с внутренностями, но там по сути ничего сложного — просто берем стили из макета. Самое необычное — это margin-left: auto у цифр количества. Это будет работать только если у родителя есть display: flex, не забывайте об этом.

.content__close {
  position: absolute;
  right: 16px;
  top: 20px;
  display: none;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
}

.content__subtitle {
  margin: 0;
  font-weight: 700;
  font-size: 24px;
  line-height: 120%;
  color: #17212b;
}

.content__inner {
  border: 1px solid #e6ebf2;
  border-radius: 16px;
  padding: 16px;
}

.content__caption {
  margin: 0;
  margin-bottom: 12px;
  font-weight: 700;
  font-size: 16px;
  line-height: 120%;
  color: #17212b;
}

.content__list {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
}

.content__btn {
  border: none;
  width: 100%;
  display: flex;
  padding: 6px 0;
  font-family: inherit;
  font-weight: 400;
  font-size: 14px;
  line-height: 120%;
  color: #5d6977;
  background: transparent;
  cursor: pointer;
}

.content__quantity {
  margin-left: auto;
}

Далее основная часть. Здесь стоит сделать grid-column, чтобы задать количество колонок. Также сразу сделаем верхний блок, в том числе кнопку для фильтров. А затем ее скроем. Пожалуй, тут важно учитывать то, как мы работаем с <svg> — удаляем у нее все атрибуты fill, а затем используем currentColor, чтобы цвет текста и цвет иконки всегда совпадали.

.content__main {
  grid-column: 9 span;
}

.content__top {
  margin-bottom: 24px;
  border-radius: 24px;
  padding: 24px;
  box-shadow: 0 15px 50px 0 rgba(0, 0, 0, 0.08);
  background: #fff;
}

.content__filter {
  display: none;
  align-items: center;
  gap: 4px;
  margin-top: 16px;
  border: none;
  padding: 0;
  font-family: inherit;
  font-weight: 400;
  font-size: 16px;
  line-height: 120%;
  color: #687585;
  background: transparent;
  transition: color 0.3s;
  cursor: pointer;
}

.content__filter svg {
  width: 16px;
  height: 16px;
  fill: currentColor;
}

.content__title {
  margin: 0;
  margin-bottom: 8px;
  font-weight: 700;
  font-size: 32px;
  line-height: 120%;
  color: #17212b;
}

.content__count {
  font-weight: 400;
  font-size: 16px;
  line-height: 120%;
  color: #687585;
}

Теперь список товаров. Здесь мы поступим необычно, и сделаем внутри список не из 9 колонок, а из 12ти. Я просто хочу показать, что так тоже можно, и что так тоже будет работать. Список — отдельный грид-родитель, делящийся на 12 колонок. А внутри элементы, которые будут занимать по 4.

.content__products {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 24px;
}

.content__products-item {
  grid-column: 4 span;
}

Остаются карточки. Здесь все берется из макета, разве что растягивание ссылки на всю карточку — отдельная история. Для этого нам нужно задать position: relative для карточки, а у ссылки сделать псевдоэлемент с position: absolute и inset: 0. Так, псевдоэлемент станет работать относительно карточки и ссылка «растянется». И да, бейджик мы делаем тоже с абсолютным позиционированием, чтобы он «висел» поверх изображения. Напоследок, конечно, не забудем сделать эффект наведения у кнопки, а также поднять ее над нашей растянутой ссылкой через z-index.

Кстати, вы могли заметить, что другим ссылкам\кнопкам я не задавал это состояние, в частности кнопке закрытия и кнопки фильтров. Это потому, что они доступны только на мобилке, где состояние наведения не работает по умолчанию.

.product {
  position: relative;
  border-radius: 24px;
  padding: 16px;
  height: 100%;
  box-shadow: 0 15px 50px 0 rgba(0, 0, 0, 0.08);
  background: #fff;
}

.product__badge {
  position: absolute;
  left: 32px;
  top: 32px;
  border-radius: 100px;
  padding: 8px 12px;
  font-weight: 700;
  font-size: 12px;
  line-height: 120%;
  color: #fff;
  background: #ff6d4d;
}

.product__pic {
  display: block;
  margin-bottom: 12px;
  border-radius: 16px;
  max-width: 100%;
  max-height: 240px;
  object-fit: cover;
}

.product__title {
  margin: 0;
  margin-bottom: 12px;
  font-weight: 700;
  font-size: 20px;
  line-height: 120%;
  color: #17212b;
}

.product__link {
  color: inherit;
  text-decoration: none;
}

.product__link::after {
  content: "";
  position: absolute;
  inset: 0;
}

.product__descr {
  margin: 0;
  margin-bottom: 12px;
  font-weight: 400;
  font-size: 16px;
  line-height: 120%;
  color: #17212b;
}

.product__price {
  margin-bottom: 12px;
  font-weight: 700;
  font-size: 24px;
  line-height: 120%;
  color: #17212b;
}

.product__btn {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 12px;
  padding: 10px 16px;
  font-weight: 400;
  font-size: 16px;
  line-height: 120%;
  color: #fff;
  background: #17212b;
  cursor: pointer;
  transition: background 0.3s;
}

.product__btn:hover {
  background: #2b4055;
}

Делаем адаптив

Переходим к важной части нашего интерфейса — адаптиву. Поскольку у нас в макете есть только мобильный макет — все, что до него мы будем делать самостоятельно. И здесь я придерживаюсь одного простого принципа: пока все выглядит хорошо — не трогаем. Как только начинает ломаться — мы возращаемся чуть назад, на безопасный брейкпоинт, и используем его.

На нашем сайте уже после ширины 1200 пикселей сайт начинает ломаться. Карточкам мало воздуха, они слишком ужаты. Именно в этот момент я и предлагаю менять расположение контента. Добавляем @media (max-width: 1200px) {}. Первым делом — сделать панель фильтров поверх сайта. Для этого используем position: fixed. Также стоит задать overflow-y: auto, чтобы у нас добавился скролл внутри самих фильтров, если их высота слишком мала. И конечно, нужно указать саму высоту — 100dvh.

Если вы еще не знакомы с данной единицей измерения, то объясню просто — мы используем этот размер для того, чтобы высота нашего окна фильтров всегда была 100% от высоты экрана, при этом не зависела от других факторов, таких как панели в браузере. То есть, при загрузке сайта и открытии панели мы увидим одно, затем вы начнете скроллить сайт, шапка браузера пропадет и высота с помощью dvh подстроится автоматически. Это и называется dynamic viewport height — динамическая высота вьюпорта, которая сама выстроится как надо.

При всем этом, разумеется, надо скрыть панель по умолчанию, и сделаем это мы через opacity: 0; visually-hidden. Так элемент и будет скрыт от пользователя, и будет скрыт от скринридеров, и позволит нам потом плавного его показать. И да, сразу после этого добавим класс активности, который добавим к элементу через js, чтобы его показать.

.content__aside {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 1000;
  border-radius: 0;
  overflow-y: auto;
  width: 100%;
  height: 100dvh;
  padding: 16px;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0.3s;
}

.content__aside--visible {
  opacity: 1;
  visibility: visible;
}

Затем мы покажем кнопку закрытия, кнопку фильтров, а также изменим сетку карточек, с 9 колонок на 12, чтобы она заняла всю ширину.

.content__close {
  display: block;
}

.content__main {
  grid-column: 12 span;
}

.content__filter {
  display: inline-flex;
}

Теперь у нас ничего не ломается, и мы можем двигаться дальше. Следующий слом происходит в районе 800 пикселей, поэтому нам стоит вернуться назад, к безопасному брейкпоинту, и использовать его. Ближайший — это 992. Здесь нам стоит сделать не по 3 карточки в ряд, а уже по две. так будет удобнее. А также растянем изображение на 100%, чтобы не было пустоты.

.content__close {
  display: block;
}

.content__main {
  grid-column: 12 span;
}

.content__filter {
  display: inline-flex;
}

Последний штрих адаптива — это мобильное разрешение. Здесь можно использовать популярный брейкпоинт 576.

@media (max-width: 576px) {
  body {
    padding: 16px 0;
  }

  .content__products-item {
    grid-column: 12 span;
  }
}

Здесь мы делаем по одной карточке в ряд, а также меняем отступы у боди.

Оживляем фильтры

Остался последний штрих — оживить фильтры на мобильном устройстве, чтобы по клику на «фильтры» открывалась панель, а по клику на крестик она закрывалась. Здесь нам потребуется JavaScript.

Сразу скажу, что даже если вы никогда не писали код на JS — не беда. Все равно почитайте этот раздел, здесь нет ничего особо сложного.

Итак, первое что нам нужно сделать, это правильно подключить js-файл к нашему html-коду. Делать это мы будем рядом с <link> внутри <head>. Подключаем скрипт через тег <script> — <script src="js/script.js" defer></script>. Здесь очень важен атрибут defer, который говорит браузеру загрузить js только тогда, когда html-код готов. Если атрибут убрать, может возникнуть ситуация, когда скрипт уже исполнен, а html еще не загрузился. В таком случае js «упадет» с ошибкой, т. к. не сможет найти элементы.

Далее, открываем js-файл и пишем функцию. Функция — это фрагмент кода на js, который выполняет какое-либо действие.

Назовем функцию initFilter. Внутри нее нам нужно сразу найти кнопку фильтра, и тут мы используем ранее добавленные в html дата-атрибуты.

const filterBtn = document.querySelector("[data-filter]");

Мы создали переменную filterBtn и написали код, который найдет элемент с атрибутом data-filter. Если перевести на простой русский язык, мы написали «документ, найти селектор [data-filter]».

После этого нам нужно выполнить проверку. Если кнопка фильтра не существует — то нам и выполнять нечего.

if (!filterBtn) return;

! означает отрицание, то есть что элемент не существует. И мы написали условие. Если элемент не существует — возвращаем пустоту через return. А это автоматически приведет к выходу из функции, и дальнейший код просто не выполнится.

Но если элемент есть — продолжим. Нужно найти еще два элемента — это сама панель, а также кнопка закрытия.

const aside = document.querySelector("[data-aside]");
const closeBtn = document.querySelector("[data-close]");

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

В JS есть метод addEventListener, который позволяет «повесить» какое-то действие на элемент. В нашем случае нужен клик.

filterBtn.addEventListener("click", () => {
    aside.classList.add('content__aside--visible');
    document.body.classList.add('stop-scroll');
  });

Здесь мы вешаем клик, а внутри него пишем, что для панели нужно добавить (add) класс. Если помните, мы добавляли этот класс выше в CSS. Обратите внимание, что класс в кавычках указан без точки, потому что здесь уже подразумевается, что это CSS-класс.

Вторым действием мы находим body и добавляем класс к нему, чтобы остановить скролл страницы.

Обратное действие ровно такое же, но меняется пара слов.

closeBtn.addEventListener("click", () => {
    aside.classList.remove("content__aside--visible");
    document.body.classList.remove("stop-scroll");
  });

Теперь мы вешаем клик на кнопку закрытия, и вместо добавления классов — удаляем их (remove).

Вот и все, теперь наш js работает. Ниже вы увидите полный код всех файлов, можете подсмотреть туда, если что.

В этом задании мы сверстали страницу каталога товаров, которая очень похожа на интерфейсы реальных интернет-магазинов. По пути разобрали семантическую структуру каталога, построили адаптивную Grid-сетку, оформили карточки товаров, реализовали боковую панель фильтров и сделали мобильную версию с открывающимся меню.

Решение задачи

предыдущий пост следующий пост