В этом мини-проекте соберём анимированную карточку товара: с увеличением изображения при наведении, кнопкой «В избранное», плавным появлением и интерактивными кнопками.
Готовая карточка работает прямо в статье. Ниже разберём её по частям — сначала HTML-разметку, затем CSS-анимации и стили, а для кнопки «В избранное» добавим немного JavaScript.
В этом разборе
Показать полный код страницы (можно скопировать целиком)
<!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 обязателен - он описывает картинку, если она не загрузится или для скринридера.
<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 на родителе.
.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 даёт ей название, ведь видимого текста внутри нет - только иконка.
<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.
.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-класса.
<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 отключает анимацию для тех, кто выбрал в системе меньше движения на экране.
.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>.
<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(). Ни одной картинки или иконочного шрифта не понадобилось.
.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 и может влиять на исходную страницу.
<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) - типичная тактильная обратная связь для кнопок покупки.
.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 и другие.