Как сверстать карточки товара на HTML и CSS

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

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

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

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

Создаем 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>

Переходим к блоку. Начнем, конечно, с обертки, и здесь логично будет использовать список — <ul>. Список дает дополнительную семантику, позволяет скринридерам читать количество элементов. Таким образом, незрячие пользователи смогут понять, сколько у вас всего карточек.

<ul class="products">
    <li class="products__item">
    </li>
    <li class="products__item">
    </li>
    <li class="products__item">
    </li>
  </ul>

Теперь сделаем карточку. Товары, как независимые блоки, которые можно переиспользовать, причем не только на текущем сайте, отлично подходят как тег <article>. И здесь сразу хочу коснуться темы «а почему не ссылка?». В данном случае, помимо ссылки, у нас в карточке есть и кнопка. А вставлять кнопку в ссылку или наоборот — ошибка. Но мы все равно реализуем клик по ссылке в любом месте карточки, только через CSS.

Внутри карточки мы сделаем изображение (не забудьте заполнить alt), заголовок, ссылку внутри заголовка (вот, куда ее лучше всего добавить), описание, а также нижний блок — <div>. В нижнем блоке будет цена и кнопка «купить». И выглядеть это будет так:

<article class="product">
  <img src="img/1.png" alt="Nova X" class="product__pic">
  <h3 class="product__title">
    <a href="#" target="_blank" class="product__link">
      Nova X
    </a>
  </h3>
  <p class="product__descr">
    OLED-дисплей и двойная камера
  </p>
  <div class="product__bottom">
    <div class="product__price">
      49 990 ₽
    </div>
    <button class="product__btn">
      Купить
    </button>
  </div>
</article>

Точно также дублируем карточки, заполняем их нужными данными, и разметка готова.

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

Подключим файл стилей через <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/InterSemiBold.woff2");
  font-display: swap;
  font-weight: 600;
}

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

body {
  margin: 0;
  font-family: "Inter", sans-serif;
  background: #282828;
}

Важный момент: мы подключаем одно семейство шрифтов Inter, поэтому в подключении и указан один и тот же font-family. Отличия между начертаниями сделаны засчет свойства font-weight.

Теперь нам стоит создать стили для списка, и тут нам понадобится flexbox. Растянем элементы с помощью flex: 1, но при этом добавим ограничители — min-width и max-width. Этот пример можно было бы сделать и с помощью обычной ширины, но там пришлось бы использовать функцию calc(), чтобы высчитать ширину.

.products {
  display: flex;
  flex-wrap: wrap;
  margin: 0 auto;
  padding: 20px;
  max-width: 980px;
  list-style: none;
  gap: 20px;
}

.products__item {
  flex: 1;
  max-width: 300px;
  min-width: 250px;
}

Далее стили карточки. Здесь есть несколько важных моментов:

  1. Мы задаем карточке position: relative. В обычной ситуации это не понадобилось бы, но если мы хотим «растянуть» ссылку на всю карточку, это обязательно.
  2. Мы задаем карточке flexbox и flex-direction: column. Это понадобится, чтобы сделать нижний блок с ценой и кнопкой всегда внизу, вне зависимости от контента карточки.
  3. Мы задаем карточке min-height, чтобы все карточки имели один размер.
.product {
  position: relative;
  display: flex;
  flex-direction: column;
  border-radius: 24px;
  padding: 16px;
  min-height: 420px;
  background: #111827;
}

Далее зададим стили внутренним элементам — изображению, заголовку, ссылке.

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

.product__title {
  margin: 0;
  font-weight: 700;
  font-size: 20px;
  color: #fff;
}

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

Из важного — это max-width: 100% у изображения, без которого большие картинки просто не будут подстраиваться под ширину карточки, а также color: inherit у ссылки. Тут дело в том, что по умолчанию в браузере ссылки — синие. И даже несмотря на то, что ссылка находится в заголовке, по умолчанию она все равно будет синей. Поэтому мы явно говорим «возьми цвет текста у родителя, то есть отнаследуй от него цвет».

Теперь сделаем описание и нижний блок, добавив «магию» flexbox, чтобы нижний блок всегда был внизу.

.product__descr {
  margin: 0;
  margin-top: 8px;
  font-weight: 400;
  font-size: 16px;
  line-height: 140%;
  color: #94a3b8;
}

.product__bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: auto;
  gap: 20px;
}

.product__price {
  font-weight: 700;
  font-size: 18px;
  color: #fff;
}

.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-family: inherit;
  font-weight: 600;
  font-size: 14px;
  color: #fff;
  background: #3b82f6;
  cursor: pointer;
  transition: background 0.3s;
}

На самом деле, никакой магии тут нет. Просто любому flex-элементу можно задать auto-margin, и он создаст отступ на все доступное пустое пространство. Если текста в карточке мало — пространства будет много, если текста много — пространства мало. Все просто.

Ну и конечно, я не мог не сделать :hover состояние на кнопку, даже если в макете его нет. И еще один важный момент у кнопки — это position: relative и z-index: 2. Но про это уместнее сказать ниже.

Последним этапом будет «растяжение» ссылки на всю карточку. Здесь стоит использовать старый и проверенный метод — псевдоэлемент. Создаем псевдоэлемент, делаем его через position: absolute, а затем «растягиваем» с помощью свойства inset. Здесь мы по сути объединили в одном свойстве сразу 4 — top, bottom, left, right. А комбинация всех этих свойств со значением 0 позволяет абсолютно позиционированному элементу стать размером с ближайшего родителя, который имеет position: relative. А это, напомню, наша карточка. Поэтому растяжение ссылки работает.

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

И тут как раз стоит упомянуть position и z-index у кнопки. Они добавлены намеренно. чтобы кнопка сама встала «поверх» ссылки, и мы могли на нее нажимать. В итоге получается, что клик по любому месту карточки кроме кнопки — это ссылка.

Теперь вы умеете создавать карточки товара с изображением, названием, описанием, ценой и кнопкой покупки.

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

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