49 Модуль 10 Реальные проекты 20 мин урок 24 из 25

Проект: интернет-магазин

Каталог с фильтрами без единой строки скрипта, галерея товара на якорях, состояние «нет в наличии» и микроразметка для поисковиков.

Что ты научишься делать

  • Сделаешь работающие фильтры каталога на :has() и чекбоксах
  • Соберёшь галерею товара с превью без JavaScript
  • Разметишь товар так, чтобы поисковик показал цену прямо в выдаче

#Что собираем

Каталог кофейни: список товаров, фильтры сбоку, карточка товара с галереей. Всё интерактивное — на CSS, потому что здесь это честно работает.

Оговорка сразу: настоящий магазин фильтрует на сервере, иначе на тысяче товаров страница станет неподъёмной. Приём из этого урока хорош для каталога на два-три десятка позиций — и как способ понять, насколько далеко зашёл современный CSS.

#Шаг 1. Сетка каталога

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

CSScatalog.css
@layer layout {  .shop {    display: grid;    grid-template-columns: 240px minmax(0, 1fr);    gap: var(--space-6);  }  @media (max-width: 860px) {    .shop { grid-template-columns: minmax(0, 1fr); }  }  .catalog {    display: grid;    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));    gap: var(--space-4);  }  /* Каждая ячейка — контейнер: карточка сама решит,     как ей собраться в доставшейся ширине */  .catalog > * {    container-type: inline-size;    container-name: product;  }}@container product (min-width: 320px) {  .product { grid-template-columns: 110px 1fr; }  .product__desc { display: block; }}

#Шаг 2. Фильтры без скриптов

Приём держится на трёх вещах: чекбоксы с состоянием, :has() у контейнера и правило, прячущее неподходящие карточки.

Логика читается почти словами: «если у каталога есть отмеченный фильтр по молочным напиткам, спрячь все карточки, которые не молочные».

Рабочие фильтры на чистом CSS
В песочницу

#Шаг 3. Сортировка порядком

Сортировку на CSS тоже можно изобразить — свойством order у флекс- или грид-элементов. Значение берётся из данных товара.

CSSСортировка по цене
/* Каждой карточке при генерации проставлен --price */.shop:has(#sort-price:checked) .product {  order: var(--price);}

#Шаг 4. Галерея товара

Крупная картинка и полоса превью. Без скриптов это делается прокруткой с привязкой: превью — обычные якорные ссылки, лента прокручивается к нужному снимку и фиксируется на нём.

Галерея на якорях и scroll-snap
В песочницу

#Шаг 5. Состояние «нет в наличии»

Товара нет — карточку не прячут, а помечают. Иначе человек будет искать её и думать, что ошибся.

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

CSSСостояние товара
@layer components {  /* Состояние в data-атрибуте: видно, что оно временное */  .product[data-stock="out"] .product__media {    filter: grayscale(1);    opacity: .6;  }  .product[data-stock="out"] .product__price {    color: var(--color-muted);    text-decoration: line-through;  }  /* Кнопка меняется целиком: покупать нечего,     но можно попросить сообщить о поступлении */  .product[data-stock="out"] .product__buy { display: none; }  .product:not([data-stock="out"]) .product__notify { display: none; }}

#Шаг 6. Микроразметка товара

Микроразметка — это данные о товаре в машиночитаемом виде. Благодаря ей поисковик показывает в выдаче цену, наличие и рейтинг прямо под ссылкой.

Проще всего добавить её отдельным блоком в формате JSON-LD: он не смешивается с вёрсткой и не мешает менять разметку.

HTMLВ конце страницы товара
<script type="application/ld+json">{  "@context": "https://schema.org",  "@type": "Product",  "name": "Раф на кокосовом молоке",  "description": "Мягкий напиток на растительном молоке",  "brand": { "@type": "Brand", "name": "Полка" },  "offers": {    "@type": "Offer",    "price": "240",    "priceCurrency": "RUB",    "availability": "https://schema.org/InStock"  }}</script>

#Корзина и её состояния

Корзина — место, где вёрстка чаще всего оказывается неготовой к реальности. Состояний у неё больше, чем кажется, и половину обычно не рисуют.

СостояниеЧто нужно предусмотреть
ПустаяОбъяснение и ссылка в каталог, а не просто «0 товаров»
Один товарРаскладка не должна выглядеть сломанной из-за одной строки
Много товаровПрокрутка внутри списка, итог всегда виден
Товар кончился, пока лежал в корзинеПометка и предложение убрать — самая забываемая ситуация
Изменение количестваИтог пересчитывается, число не дёргается: tabular-nums
Отправка заказаКнопка блокируется, показывается ход выполнения

#Счётчик товаров в шапке

Мелочь, на которой видно разницу между «работает» и «сделано хорошо». Счётчик меняется во время работы страницы — значит это живая область из сорок третьего урока.

HTMLДоступный счётчик
<a class="cart" href="/cart">  <svg class="icon" aria-hidden="true"><use href="#i-cart"/></svg>  <!-- Число видно глазами -->  <span class="cart__count" aria-hidden="true">3</span>  <!-- А это слышит читалка: полная фраза вместо голой цифры -->  <span class="visually-hidden" role="status">    Корзина: 3 товара  </span></a>

#Грабли этого проекта

Что случилосьПочемуРешение
Фильтры перестали складыватьсяКомбинаций условий больше, чем правилОграничить число фильтров либо перенести на сервер
Карточка сжалась в узкой колонкеМедиазапрос смотрит на экран, а не на колонкуКонтейнерные запросы у ячеек каталога
Сортировка сбила порядок чтенияorder меняет вид, но не разметкуСортировать на сервере или менять сам порядок элементов
Галерея не листается с клавиатурыПревью сделаны из divНастоящие ссылки на якоря снимков
Поисковик не показал ценуДанные в разметке расходятся с видимымиПривести к одному значению
Пропавшие товары путаютОтсутствующие позиции скрыты полностьюПомечать, а не прятать

Где спотыкаются почти все

Фильтры на CSS в большом каталоге

Почему Скрытые карточки остаются в разметке: тысяча товаров грузится вся целиком. Плюс комбинации условий растут быстрее, чем их можно описать.

Как надо До нескольких десятков позиций — можно. Дальше только сервер.

Сортировка через <code>order</code>

Почему Видимый порядок расходится с порядком обхода клавиатурой и чтения голосом.

Как надо Менять сам порядок элементов, а не только их отображение.

Товар без наличия скрыт из каталога

Почему Человек помнит, что видел позицию, и решает, что ошибся страницей.

Как надо Показывать с пометкой и предложением сообщить о поступлении.

Микроразметка не совпадает со страницей

Почему Поисковики это проверяют и перестают показывать расширенные сведения для всего сайта.

Как надо Генерировать разметку из тех же данных, что и видимую цену.

Привязка прокрутки scroll-snap
Остановка прокрутки на заданных точках — основа галерей без скриптов.
Микроразметка structured data
Данные о содержимом в машиночитаемом виде для поисковиков.
Фасетный фильтр faceted filter
Набор независимых условий отбора, работающих совместно.
Расширенные сведения rich results
Цена, рейтинг и наличие в поисковой выдаче под ссылкой.

Проверь себя

  1. Почему фильтры на CSS не годятся для большого каталога?

    Приём хорош на паре десятков позиций. Дальше страница становится тяжёлой, а правил — неуправляемо много.

  2. Чем плоха сортировка через свойство <code>order</code>?

    Человек видит один порядок, а слышит другой. Для сортировки это неприемлемо.

  3. Почему превью в галерее — ссылки, а не div?

    Всё поведение встроено в браузер — оно не сломается при ошибке в скрипте.

  4. Что делать с товаром, которого нет в наличии?

    Исчезнувшая позиция путает: человек помнит, что видел её, и думает, что ошибся.

  5. Что произойдёт, если цена в микроразметке не совпадёт с видимой?

    Расхождение проверяется. Санкция распространяется на весь сайт, а не только на одну страницу.

Задание

Фильтр каталога

В каталоге четыре товара с атрибутами data-type и data-stock. Фильтры уже размечены, но не работают.

Напиши правила на :has(): отмеченный фильтр «С молоком» прячет всё, что не молочное; «Только в наличии» — всё, чего нет. Плюс оформи состояние «нет в наличии»: перечёркнутая приглушённая цена.

Открыть в песочнице
    Подсказка 1

    Условие вешается на общий контейнер: .shop:has(#f-milk:checked) .product:not([data-type="milk"]) { display: none; }

    Подсказка 2

    Второй фильтр — по тому же образцу, только с #f-stock и [data-stock="yes"].

    Подсказка 3

    Состояние: .product[data-stock="no"] .price { color: #7a7f8c; text-decoration: line-through; }

    Показать решение
    HTMLindex.html
    <div class="shop">  <form class="filters">    <label><input type="checkbox" id="f-milk"> С молоком</label>    <label><input type="checkbox" id="f-stock"> Только в наличии</label>  </form>  <div class="catalog">    <article class="product" data-type="milk" data-stock="yes"><h3>Латте</h3><p class="price">210 ₽</p></article>    <article class="product" data-type="black" data-stock="yes"><h3>Эспрессо</h3><p class="price">120 ₽</p></article>    <article class="product" data-type="milk" data-stock="no"><h3>Раф</h3><p class="price">240 ₽</p></article>    <article class="product" data-type="black" data-stock="no"><h3>Фильтр</h3><p class="price">180 ₽</p></article>  </div></div>
    CSSstyle.css
    /* Отмечен фильтр — прячем то, что ему не подходит */.shop:has(#f-milk:checked) .product:not([data-type="milk"]) {  display: none;}.shop:has(#f-stock:checked) .product:not([data-stock="yes"]) {  display: none;}/* Товара нет: помечаем, но не прячем */.product[data-stock="no"] .price {  color: #7a7f8c;  text-decoration: line-through;}.shop { display: grid; grid-template-columns: 150px 1fr; gap: 18px; }.filters { display: grid; gap: 8px; align-content: start; font-size: 14px; }.filters label { display: flex; align-items: center; gap: 8px; }.catalog { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; align-content: start; }.product {  display: grid;  gap: 4px;  align-content: start;  padding: 12px;  background: #f5f5fa;  border-radius: 10px;}.product h3 { margin: 0; font-size: 15px; }.price { margin: 0; font-weight: 700; }

    Чек-лист сдачи магазина