Мини-проект: анимированная карточка товара

В этом мини-проекте соберём анимированную карточку товара: с увеличением изображения при наведении, кнопкой «В избранное», плавным появлением и интерактивными кнопками.

Готовая карточка работает прямо в статье. Ниже разберём её по частям — сначала HTML-разметку, затем CSS-анимации и стили, а для кнопки «В избранное» добавим немного JavaScript.

Показать полный код страницы (можно скопировать целиком)
tovar-demo.html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Карточка товара - демо</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@200;400;600;700;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
  :root {
    --paper: #faf6f0;
    --ink: #1c1a17;
    --dim: #7a7468;
    --accent: #e8590c;
    --heart: #e23a5e;
    --line: #e4ddd0;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: 'Manrope', 'Segoe UI', Arial, sans-serif;
    display: flex;
    justify-content: center;
    padding: 54px 20px;
  }
  a { text-decoration: none; color: inherit; }
 
  .card-page { width: 100%; max-width: 380px; }
 
  /* карточка появляется один раз при загрузке страницы */
  .product-card {
    opacity: 0;
    transform: scale(.94) translateY(14px);
    animation: card-in .6s cubic-bezier(.16, 1, .3, 1) forwards;
    background: #fff;
    border: 1px solid var(--line);
    border-radius: 20px;
    overflow: hidden;
    box-shadow: 0 14px 34px rgba(28, 26, 23, .07);
    transition: box-shadow .35s ease, transform .35s ease;
  }
  @keyframes card-in {
    to { opacity: 1; transform: none; }
  }
  .product-card:hover {
    box-shadow: 0 26px 54px rgba(28, 26, 23, .14);
    transform: translateY(-6px);
  }
 
  /* медиа-блок с картинкой, бейджем и кнопкой избранного */
  .card-media {
    position: relative;
    overflow: hidden;
    aspect-ratio: 4 / 3;
    background: var(--paper);
  }
  .card-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform .5s ease;
  }
  .product-card:hover .card-media img {
    transform: scale(1.08);
  }
  .badge {
    position: absolute;
    z-index: 2;
    top: 14px;
    left: 14px;
    padding: 6px 12px;
    background: rgba(28, 26, 23, .78);
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    border-radius: 999px;
  }
 
  .wishlist-btn {
    position: absolute;
    z-index: 2;
    top: 10px;
    right: 10px;
    width: 38px;
    height: 38px;
    border: none;
    border-radius: 50%;
    background: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
    box-shadow: 0 6px 16px rgba(28, 26, 23, .18);
  }
  .wishlist-btn svg {
    width: 19px;
    height: 19px;
    fill: none;
    stroke: var(--dim);
    stroke-width: 2;
    transition: stroke .25s ease;
  }
  .wishlist-btn.active svg {
    fill: var(--heart);
    stroke: var(--heart);
    animation: heart-pop .5s ease;
  }
  @keyframes heart-pop {
    0%   { transform: scale(1); }
    35%  { transform: scale(1.35); }
    60%  { transform: scale(.9); }
    100% { transform: scale(1); }
  }
 
  /* текстовая часть карточки */
  .card-body { padding: 18px 20px 20px; }
  .card-category {
    display: block;
    font-size: 11.5px;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--dim);
    font-weight: 700;
  }
  .card-body h3 {
    margin: 6px 0 8px;
    font-size: 19px;
    font-weight: 800;
  }
  .card-desc {
    margin: 0 0 14px;
    font-size: 13.5px;
    line-height: 1.55;
    color: var(--dim);
  }
 
  .feature-list {
    list-style: none;
    margin: 0 0 18px;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .feature-list li {
    position: relative;
    padding-left: 22px;
    font-size: 13px;
    color: var(--ink);
  }
  .feature-list li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 2px;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: rgba(232, 89, 12, .12);
  }
  .feature-list li::after {
    content: "";
    position: absolute;
    left: 4.5px;
    top: 6px;
    width: 5px;
    height: 8px;
    border-right: 2px solid var(--accent);
    border-bottom: 2px solid var(--accent);
    transform: rotate(45deg);
  }
 
  .price-row {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 14px;
  }
  .price {
    font-family: 'JetBrains Mono', monospace;
    font-size: 26px;
    font-weight: 700;
  }
  .price-note { font-size: 12px; color: var(--dim); }
 
  .cta-row { display: flex; gap: 10px; }
  .btn {
    flex: 1;
    text-align: center;
    padding: 13px 10px;
    border-radius: 10px;
    font-size: 13.5px;
    font-weight: 700;
    transition: transform .15s ease, background .2s ease;
  }
  .btn-buy { background: var(--accent); color: #fff; }
  .btn-buy:hover { background: #c94a09; }
  .btn-buy:active { transform: scale(.96); }
  .btn-demo { border: 1px solid var(--line); color: var(--ink); }
  .btn-demo:hover { background: var(--paper); }
  .btn-demo:active { transform: scale(.96); }
 
  @media (prefers-reduced-motion: reduce) {
    * { animation-duration: .001ms !important; transition-duration: .001ms !important; }
    .product-card { opacity: 1; transform: none; }
  }
</style>
</head>
<body>
 
<main class="card-page">
 
  <article class="product-card">
 
    <div class="card-media">
      <span class="badge">Шаблон Blogger</span>
      <button type="button" class="wishlist-btn" aria-pressed="false" aria-label="Добавить в избранное">
        <svg viewBox="0 0 24 24"><path d="M12 21s-7.5-4.6-10-9.1C.4 8.6 1.9 5 5.4 4.2 7.8 3.6 10 4.8 12 7c2-2.2 4.2-3.4 6.6-2.8 3.5.8 5 4.4 3.4 7.7C19.5 16.4 12 21 12 21z"/></svg>
      </button>
      <img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYYGEnikDy6NN4mNlJjqV0GeE6k-5kpgb2sl9iA-M3LwhwmT5AYdeSwH3urUROkKkdb3BPPbtY6lpi4ZDhhgiYh58IYEmD99zNulIHURT9oRHm7_yIDC4-hR7diZEurLZI1r8nLyfvFn0cM2TPkFARCK5dbAD8bZDdVyCwxKBlu6ckLciqkJdkmkndJGDo/s1600/Shopping%20Blogger%20Template.jpg" alt="Превью шаблона Blogger Shopping - витрина интернет-магазина">
    </div>
 
    <div class="card-body">
      <span class="card-category">Интернет-магазин / Blogger</span>
      <h3>Shopping</h3>
      <p class="card-desc">Шаблон-витрина для Blogger. Каталог товаров с закладками и внешними ссылками, адаптивный дизайн для заработка на CPA.</p>
 
      <ul class="feature-list">
        <li>Избранное без нагрузки на сервер</li>
        <li>Ценники и наклейки скидок через шорткоды</li>
        <li>Тёмная тема и липкий сайдбар</li>
      </ul>
 
      <div class="price-row">
        <span class="price">$21</span>
        <span class="price-note">разовая покупка</span>
      </div>
 
      <div class="cta-row">
        <a class="btn btn-buy" href="https://bloggerstore.gumroad.com/l/shopping?a=802829075" target="_blank" rel="noopener">Купить</a>
        <a class="btn btn-demo" href="https://shopping-pikitemplates.blogspot.com/" target="_blank" rel="noopener">Демо</a>
      </div>
    </div>
 
  </article>
 
</main>
 
<script>
  (function () {
    var btn = document.querySelector('.wishlist-btn');
    btn.addEventListener('click', function () {
      var active = btn.classList.toggle('active');
      btn.setAttribute('aria-pressed', active ? 'true' : 'false');
      btn.setAttribute('aria-label', active ? 'Убрать из избранного' : 'Добавить в избранное');
    });
  })();
</script>
 
</body>
</html>

Карточка и медиа-блок: тень и увеличение картинки при наведении

Вся карточка - один тег <article>, потому что это самостоятельный блок контента, который имеет смысл и отдельно от страницы. Внутри - .card-media: обёртка для картинки с overflow: hidden, которая работает "окном" - увеличенная по наведению картинка не вылезает за скруглённые края.

HTML: что выводит эта разметка

<article class="product-card"> - карточка как самостоятельный блок; внутри .card-media с бейджем и картинкой товара. Атрибут alt у img обязателен - он описывает картинку, если она не загрузится или для скринридера.

HTML
<article class="product-card">
  <div class="card-media">
    <span class="badge">Шаблон Blogger</span>
    <!-- кнопка "в избранное" - см. следующий блок -->
    <img src="..." alt="Превью шаблона Blogger Shopping">
  </div>
  ...
</article>

Подробнее: nav, article, section, aside и Изображения: тег img и атрибут alt.

CSS: тень и масштаб при наведении

У самой карточки box-shadow становится глубже при :hover, а сама она чуть приподнимается через translateY(-6px) - эффект "карточка тянется к курсору". Отдельно, только у картинки внутри .card-media, срабатывает transform: scale(1.08) - она увеличивается, но обрезается по границе обёртки за счёт overflow: hidden на родителе.

CSS
.product-card {
  box-shadow: 0 14px 34px rgba(28,26,23,.07);
  transition: box-shadow .35s ease, transform .35s ease;
}
.product-card:hover {
  box-shadow: 0 26px 54px rgba(28,26,23,.14);
  transform: translateY(-6px);
}
 
.card-media { position: relative; overflow: hidden; aspect-ratio: 4/3; }
.card-media img { transition: transform .5s ease; }
.product-card:hover .card-media img { transform: scale(1.08); }

Подробнее: Тени: box-shadow, Масштабирование: scale, scaleX, scaleY и Анимация при наведении: практика с transition и :hover.

Кнопка «в избранное» и анимация сердечка

Настоящая функция шаблона Shopping - избранное без нагрузки на сервер. Здесь она показана как рабочая кнопка: клик красит сердечко и запускает "прыжок" через @keyframes, второй клик возвращает всё обратно. Никакого бэкенда - это чисто фронтенд-демонстрация приёма.

HTML: что выводит эта разметка

<button>, а не ссылка или div - потому что это интерактивный элемент управления, не переход по адресу. aria-pressed сообщает вспомогательным технологиям, что кнопка - переключатель с двумя состояниями, а aria-label даёт ей название, ведь видимого текста внутри нет - только иконка.

HTML
<button type="button" class="wishlist-btn"
        aria-pressed="false" aria-label="Добавить в избранное">
  <svg viewBox="0 0 24 24"><path d="..."/></svg>
</button>

Подробнее: Кнопки: button vs input type=submit и ARIA-атрибуты: когда они нужны.

CSS + JS: цвет, прыжок и позиция поверх картинки

Кнопка стоит поверх картинки через position: absolute - и ей задан z-index: 2. Это не для красоты: как только картинка увеличивается по наведению через transform, браузер создаёт для неё отдельный слой наложения, и без явного z-index она в этот момент перекрыла бы кнопку сверху. JS всего один обработчик клика - переключает класс .active, а всю анимацию "прыжка" и смену цвета делает уже CSS через @keyframes.

CSS + JS
.wishlist-btn { position: absolute; z-index: 2; top: 10px; right: 10px; }
.wishlist-btn svg { stroke: var(--dim); transition: stroke .25s ease; }
 
.wishlist-btn.active svg {
  fill: var(--heart);
  stroke: var(--heart);
  animation: heart-pop .5s ease;
}
@keyframes heart-pop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.35); }
  60%  { transform: scale(.9); }
  100% { transform: scale(1); }
}
 
btn.addEventListener('click', function () {
  var active = btn.classList.toggle('active');
  btn.setAttribute('aria-pressed', active ? 'true' : 'false');
});

Подробнее: Z-index и порядок наложения, Правило @keyframes: создаём анимацию с нуля и Длительность и повтор: animation-duration и animation-iteration-count.

Плавное появление карточки при загрузке

Карточка не выскакивает на странице резко, а мягко "выплывает": одновременно проявляется (opacity) и слегка увеличивается с сдвигом вверх. В отличие от визитки из предыдущего мини-проекта, где карточки появлялись друг за другом по очереди, здесь карточка одна - и появляется одним плавным движением, без ступенчатой задержки.

HTML: что выводит эта разметка

Никакой новой разметки не требуется - анимация вешается прямо на уже знакомый тег <article class="product-card">. Это и есть польза семантического тега: он одновременно и смысловой контейнер, и точка подключения для CSS-класса.

HTML
<article class="product-card">
  ...
</article>

Подробнее: nav, article, section, aside.

CSS: @keyframes и animation-fill-mode

Правило @keyframes card-in описывает только конечное состояние (to) - карточка полностью проявлена и без сдвига; начальное состояние (полупрозрачная, уменьшенная и сдвинутая) задано прямо в .product-card через opacity и transform. Ключевое слово forwards в сокращённой записи animation удерживает финальный кадр после окончания - без него карточка вернулась бы к исходным (невидимым) стилям. Медиа-запрос prefers-reduced-motion отключает анимацию для тех, кто выбрал в системе меньше движения на экране.

CSS
.product-card {
  opacity: 0;
  transform: scale(.94) translateY(14px);
  animation: card-in .6s cubic-bezier(.16,1,.3,1) forwards;
}
@keyframes card-in {
  to { opacity: 1; transform: none; }
}
 
@media (prefers-reduced-motion: reduce) {
  .product-card { opacity: 1; transform: none; }
}

Подробнее: Анимация появления элементов при загрузке страницы, Что происходит до и после: animation-fill-mode и prefers-reduced-motion: уважение к выбору пользователя.

Информация о товаре и список возможностей

Реальное описание шаблона и три его настоящих возможности - без выдуманных характеристик. Список оформлен не стандартными точками, а галочками, нарисованными чистым CSS.

HTML: что выводит эта разметка

Категория и заголовок - обычные span и h3 (не h1, потому что заголовок товара внутри страницы - не главный заголовок документа). Возможности товара - маркированный список <ul>: это набор равнозначных пунктов, а не пронумерованная последовательность, поэтому не <ol>.

HTML
<div class="card-body">
  <span class="card-category">Интернет-магазин / Blogger</span>
  <h3>Shopping</h3>
  <p class="card-desc">Шаблон-витрина для Blogger...</p>
 
  <ul class="feature-list">
    <li>Избранное без нагрузки на сервер</li>
    <li>Ценники и наклейки скидок через шорткоды</li>
    <li>Тёмная тема и липкий сайдбар</li>
  </ul>
</div>

Подробнее: Списки: ul, ol и вложенные списки.

CSS: галочка через два псевдоэлемента

У каждого пункта убран стандартный маркер (list-style: none), а вместо него - кружок-подложка через ::before и сама галочка через ::after: это просто уголок из двух границ (border-right и border-bottom), повёрнутый на 45 градусов свойством transform: rotate(). Ни одной картинки или иконочного шрифта не понадобилось.

CSS
.feature-list { list-style: none; padding: 0; }
.feature-list li { position: relative; padding-left: 22px; }
 
.feature-list li::before {
  content: "";
  position: absolute; left: 0; top: 2px;
  width: 14px; height: 14px; border-radius: 50%;
  background: rgba(232,89,12,.12);
}
.feature-list li::after {
  content: "";
  position: absolute; left: 4.5px; top: 6px;
  width: 5px; height: 8px;
  border-right: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  transform: rotate(45deg);
}

Подробнее: Псевдоэлементы ::before и ::after и Как повернуть элемент в CSS: rotate.

Цена и кнопки «Купить» и «Демо»

Обе кнопки ведут на настоящие адреса: "Купить" - на страницу шаблона на Gumroad, "Демо" - на его живую демо-версию на Blogger. Это обычные ссылки <a>, оформленные под кнопки, а не элементы <button> - потому что они не выполняют действие на странице, а переводят на другой адрес.

HTML: что выводит эта разметка

У обеих ссылок стоит target="_blank", чтобы внешний сайт открылся в новой вкладке, не уводя человека со страницы с уроком. Атрибут rel="noopener" рядом с target="_blank" - обязательная пара: без него открытая вкладка получает доступ к window.opener и может влиять на исходную страницу.

HTML
<div class="price-row">
  <span class="price">$21</span>
  <span class="price-note">разовая покупка</span>
</div>
 
<div class="cta-row">
  <a class="btn btn-buy" href="https://bloggerstore.gumroad.com/l/shopping?a=802829075"
     target="_blank" rel="noopener">Купить</a>
  <a class="btn btn-demo" href="https://shopping-pikitemplates.blogspot.com/"
     target="_blank" rel="noopener">Демо</a>
</div>

Подробнее: Ссылки: тег a и его атрибуты.

CSS: ряд кнопок на Flexbox и обратная связь при нажатии

.cta-row - Flexbox-контейнер, а flex: 1 у каждой кнопки растягивает их поровну на всю ширину ряда, что бы ни было длиннее - "Купить" или "Демо". При наведении фон кнопки чуть темнеет через transition, а в момент нажатия (:active) кнопка на мгновение сжимается - transform: scale(.96) - типичная тактильная обратная связь для кнопок покупки.

CSS
.cta-row { display: flex; gap: 10px; }
.btn { flex: 1; transition: transform .15s ease, background .2s ease; }
 
.btn-buy { background: var(--accent); color: #fff; }
.btn-buy:hover { background: #c94a09; }
.btn-buy:active { transform: scale(.96); }

Подробнее: Что такое Flexbox и зачем он нужен и Псевдоклассы: :hover, :focus, :nth-child и другие.

Поделиться в социальных сетях

Отправить комментарий

0 Комментарии

Отправить комментарий (0)

#buttons=(Хорошо) #days=(20)

Мы используем файлы cookie для улучшения работы сайта. Подробнее
Хорошо
×