Разработка сравнения товаров для интернет-магазина
Функция сравнения товаров на карточке — это встроенный механизм добавления в список сравнения, интегрированный в контекст просмотра каталога и отдельных товаров. В отличие от отдельной системы сравнения, здесь речь идёт о UX-компоненте: кнопке, плавающей панели и странице сравнения, плотно встроенных в основной интерфейс магазина.
Точки входа в сравнение
Пользователь может добавить товар в сравнение из нескольких мест:
Карточка в листинге: небольшая кнопка или иконка рядом с «В корзину». На десктопе — появляется при hover, на мобайле — всегда видна. Не должна конкурировать по размеру с основным CTA (кнопкой покупки).
Страница товара: кнопка «Сравнить» рядом с разделом характеристик или в блоке secondary actions.
Страница сравнения: кнопка «Добавить ещё» — открывает поиск или переход в каталог.
Состояние кнопки: добавлен/не добавлен — синхронизировано глобально. При добавлении с листинга кнопка на странице товара тоже отражает состояние.
Хранение состояния
// Zustand store для списка сравнения
interface CompareStore {
items: number[]; // массив product_id
maxItems: number; // лимит (обычно 3–5)
add: (id: number) => void;
remove: (id: number) => void;
clear: () => void;
has: (id: number) => boolean;
}
const useCompareStore = create<CompareStore>()(
persist(
(set, get) => ({
items: [],
maxItems: 4,
add: (id) => {
const { items, maxItems } = get();
if (items.length >= maxItems) {
// Показываем уведомление вместо молчаливого игнора
toast.error(`Можно сравнивать не более ${maxItems} товаров`);
return;
}
if (!items.includes(id)) set({ items: [...items, id] });
},
remove: (id) => set({ items: get().items.filter(i => i !== id) }),
clear: () => set({ items: [] }),
has: (id) => get().items.includes(id),
}),
{ name: 'compare-list' } // сохраняем в localStorage
)
);
Плавающая панель сравнения
Пока пользователь листает каталог и добавляет товары, в нижней части экрана появляется fixed-панель с текущим списком.
function CompareBar() {
const { items, remove, clear } = useCompareStore();
if (items.length === 0) return null;
return (
<div className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t shadow-lg p-4
translate-y-0 transition-transform duration-300">
<div className="max-w-screen-xl mx-auto flex items-center gap-4">
<span className="text-sm text-gray-500">
Сравнение: {items.length} товара
</span>
<div className="flex gap-2 flex-1">
{items.map(id => (
<CompareBarItem key={id} productId={id} onRemove={() => remove(id)} />
))}
</div>
<Link href={`/compare?ids=${items.join(',')}`}>
<Button>Сравнить</Button>
</Link>
<button onClick={clear} className="text-gray-400 hover:text-gray-600">
Очистить
</button>
</div>
</div>
);
}
CompareBarItem — маленькое фото + название + кнопка удаления. Название обрезается до 2–3 слов. При добавлении нового товара — анимация (товар «прилетает» в панель).
Страница сравнения
Данные товаров загружаются по массиву ID из URL:
// /compare?ids=42,117,203
const ids = searchParams.get('ids')?.split(',').map(Number) ?? [];
const { data: products } = useSWR(
ids.length ? `/api/compare?ids=${ids.join(',')}` : null,
fetcher
);
API-endpoint возвращает товары с полным набором атрибутов для сравнения. Важно: если ID не существует или товар снят с продажи — возвращаем частичные данные с флагом unavailable, а не ошибку.
Таблица сравнения
Специфика ecommerce-сравнения (в отличие от отдельной системы сравнения, рассмотренной в другом разделе): здесь пользователь обычно сравнивает 2–4 товара одной категории, поэтому атрибуты более однородные.
Ключевые UX-паттерны:
Закрепление шапки с фото и ценами при вертикальном скролле:
.compare-header {
position: sticky;
top: var(--navbar-height);
z-index: 10;
background: white;
}
Подсветка различий: строки, где значения отличаются — выделяются (фон, bold). Строки с одинаковыми значениями — сворачиваются или отображаются приглушённо.
Кнопки действий в шапке: «В корзину» / «Удалить из сравнения» прямо под фото каждого товара, без необходимости переходить на карточку.
«Добавить ещё»: последняя колонка — placeholder с поиском. Пользователь может добавить товар прямо со страницы сравнения через inline-поиск.
Сравнение с контекстом: «лучший выбор»
Опциональная фича: система помечает «победителя» в каждой характеристике. Реализация через флаг highlight_if_best в атрибуте + логику определения лучшего значения (min/max для числовых).
function CompareCell({ value, isBest, attributeDirection }: Props) {
return (
<td className={cn('p-3 text-center', isBest && 'bg-green-50 font-semibold text-green-700')}>
{value}
{isBest && <span className="ml-1 text-xs">✓</span>}
</td>
);
}
Не применять к атрибутам типа «цвет», «материал» — для них «лучшего» не существует.
Синхронизация между вкладками
Если пользователь открыл несколько вкладок, список сравнения должен быть одинаковым. LocalStorage по умолчанию не уведомляет другие вкладки. Решение — storage event:
window.addEventListener('storage', event => {
if (event.key === 'compare-list') {
useCompareStore.getState().hydrate(JSON.parse(event.newValue ?? '{}'));
}
});
Zustand с persist-плагином обрабатывает это автоматически при правильной настройке.
SEO-аспекты
Страницы сравнения с конкретными ID (/compare?ids=42,117) — закрыть от индексации (noindex). Они не дают SEO-ценности и создают дубли. Canonical — нет смысла проставлять.
Если генерируются «популярные сравнения» с редакционным контентом («Сравнение iPhone 15 vs Samsung S24»), такие страницы — статичные, со своим URL и уникальным текстом. Они индексируются и ранжируются по сравнительным запросам.
Аналитика
- Какие товары чаще всего сравнивают вместе — сигнал для «похожих товаров»
- Конверсия со страницы сравнения: из каких пар заказывают, а из каких уходят
- Какой товар чаще «побеждает» в сравнениях (берут его, а не альтернативу)
-- Пары товаров, которые чаще всего сравнивают
SELECT
LEAST(product_a, product_b) AS p1,
GREATEST(product_a, product_b) AS p2,
COUNT(*) AS compare_sessions
FROM compare_sessions
GROUP BY 1, 2
ORDER BY 3 DESC;
Сроки
- Кнопка + localStorage + плавающая панель: 3–5 рабочих дней
- Страница сравнения с таблицей атрибутов и подсветкой различий: 1–2 недели
- С «лучшим выбором», inline-добавлением, аналитикой: 2–3 недели







