Developing Product Comparison Feature for E-commerce
Product comparison on product cards — built-in mechanism for adding to comparison list, integrated into catalog browsing and product page context. Unlike standalone comparison system, here we're talking about UX component: button, floating panel, and comparison page tightly integrated into store interface.
Entry Points for Comparison
User can add product to comparison from several places:
Card in listing: small button or icon next to "Add to cart". On desktop — appears on hover, on mobile — always visible. Must not compete in size with main CTA (purchase button).
Product page: "Compare" button next to specs section or in secondary actions block.
Comparison page: "Add more" button — opens search or catalog navigation.
Button state: added/not added — synchronized globally. When added from listing, button on product page also reflects state.
State Management
// Zustand store for comparison list
interface CompareStore {
items: number[]; // array of product_id
maxItems: number; // limit (usually 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(`Can compare max ${maxItems} products`);
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' } // save to localStorage
)
);
Floating Comparison Panel
As user browses catalog and adds products, a fixed panel appears at bottom showing current list.
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">
Comparing: {items.length} product
</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>Compare</Button>
</Link>
<button onClick={clear} className="text-gray-400 hover:text-gray-600">
Clear
</button>
</div>
</div>
);
}
CompareBarItem — small photo + name + delete button. Name truncated to 2–3 words. When new product added — animation (product "flies" into panel).
Comparison Page
Product data loaded by ID array from 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 returns products with full set of comparison attributes. Important: if ID doesn't exist or product discontinued — return partial data with unavailable flag, not error.
Comparison Table
E-commerce comparison specifics (unlike standalone system): user typically compares 2–4 products in one category, so attributes more homogeneous.
Key UX patterns:
Header lock with photo and prices on vertical scroll:
.compare-header {
position: sticky;
top: var(--navbar-height);
z-index: 10;
background: white;
}
Highlight differences: rows where values differ — highlighted (background, bold). Rows with same values — collapsed or muted.
Action buttons in header: "Add to cart" / "Remove from comparison" directly under each product photo, no need to navigate to card.
"Add more": last column — placeholder with search. User can add product directly from comparison page via inline search.
Comparison with Context: "Best Choice"
Optional feature: system marks "winner" in each characteristic. Implementation via highlight_if_best flag in attribute + logic to determine best value (min/max for numeric).
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>
);
}
Don't apply to attributes like "color", "material" — no "best" exists for them.
Cross-Tab Synchronization
If user opened multiple tabs, comparison list should be identical. localStorage doesn't notify other tabs by default. Solution — storage event:
window.addEventListener('storage', event => {
if (event.key === 'compare-list') {
useCompareStore.getState().hydrate(JSON.parse(event.newValue ?? '{}'));
}
});
Zustand with persist plugin handles this automatically with correct setup.
SEO Aspects
Comparison pages with specific IDs (/compare?ids=42,117) — block from indexing (noindex). Give no SEO value and create dupes. No canonical needed.
If "popular comparisons" with editorial content ("iPhone 15 vs Samsung S24") generated, such pages — static, own URL, unique text. They indexed and ranked by comparison queries.
Analytics
- Which products most often compared together — signal for "similar products"
- Conversion from comparison page: which pairs convert, which bounce
- Which product "wins" in comparisons (users buy it, not alternative)
-- Product pairs most often compared
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;
Timeline
- Button + localStorage + floating panel: 3–5 business days
- Comparison page with attributes table and difference highlighting: 1–2 weeks
- With "best choice", inline adding, analytics: 2–3 weeks







