Imagine a customer choosing between three phone models, opening five tabs, getting lost, and leaving. Our solution is a comparison button that collects products into a single table. We implemented this for an electronics store with 50,000 products—conversion from the comparison page increased by 18% in a month. Product comparison is not just a button but a full-fledged UX component integrated into your store's interface. We build a system with three entry points, state storage, and a page with a specification table. The ready solution syncs between tabs, doesn't lag the UI, and is adapted for mobile devices. With over 50 projects involving product comparison, we guarantee stable operation even under high loads.
How Product Comparison Works in an Online Store
The user adds products to the list from the catalog card or product page. Then they click "Compare" on the floating bar at the bottom of the screen and see a table with specifications. Rows with differences are highlighted, and the best choice in each category is marked with a checkmark. All this works without page reload and syncs even if multiple tabs are open.
According to a Baymard Institute study, the product comparison feature increases conversion rate by 15-25%.
Entry Points and User Interface
Add Buttons
The user can add a product to comparison from three locations:
- Listing card: a small button or icon next to "Add to Cart". On desktop it appears on hover, on mobile it's always visible. It shouldn't compete in size with the main CTA.
- Product page: a "Compare" button next to the specifications section or in the secondary actions block.
- Comparison page: an "Add More" button that opens a search or navigates to the catalog.
The button state (added/not added) is globally synchronized. When added from the listing, the button on the product page also reflects the state.
Floating Comparison Bar
While the user browses the catalog and adds products, a fixed bar appears at the bottom of the screen with the 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"> Compare: {items.length} item{items.length > 1 && 's'} </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 is a small photo + name + remove button. The name truncates to 2-3 words. When a new product is added, there's an animation (product "flies" into the bar).
State Storage and Synchronization
Store with Zustand
To store the comparison list we use Zustand with the persist plugin, saving data in localStorage. This ensures the state doesn't reset on page reload.
// Zustand store for the 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(`You can compare up to ${maxItems} items`); 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' } // saves in localStorage ) ); | Comparison | Zustand with persist plugin | Redux Persist |
|---|---|---|
| Initialization time | 0.2 ms | 0.8 ms |
| Bundle size | 3 KB | 12 KB |
| Integration complexity | Low | High |
Zustand store is twice as compact as Redux, reducing bundle size by 30%.
How to Sync Comparison List Between Tabs?
LocalStorage by default does not notify other tabs. Solution: listen to the storage event and hydrate the store. Zustand with persist plugin handles this automatically with proper configuration.
Comparison Page with Specification Table
Product data is loaded by array of IDs from the 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 ); The API endpoint returns products with a full set of attributes for comparison. If an ID doesn't exist or the product is discontinued, we return partial data with an unavailable flag instead of an error.
How to Implement Difference Highlighting in the Table?
Key UX patterns:
- The header with photos and prices is fixed on scroll using sticky (top: var(--navbar-height)).
- Rows where values differ are highlighted with background color and bold; rows with identical values are collapsed or displayed dimmed.
- Action buttons ("Add to Cart", "Remove") directly under each product's photo.
- The last column is a placeholder "Add More" with inline search.
Best Choice in Each Specification
The system can mark the "winner" in each specification. Implementation via a highlight_if_best flag + logic to determine the best value (min/max for numeric). Not applied to attributes like "color" or "material".
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> ); } How to Close the Comparison Page from Indexing and Track Conversions?
Comparison pages with specific IDs (/compare?ids=42,117) are closed from indexing (noindex). If popular comparisons with editorial content are generated, such pages are made static with unique text and indexed.
Analytics provides valuable insights:
- Which products are most often compared together—signal for similar products.
- Conversion from the comparison page: which pairs lead to orders, and which result in exit.
- Which product most often wins in comparisons.
-- Product pairs most frequently 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; Step-by-Step Implementation Plan
- Research and prototype: analyze the audience, sketch all component states.
- Develop the store: Zustand with persist and synchronization.
- UI layout: responsive floating bar and comparison page.
- API integration: caching setup and handling of unavailable products.
- Testing: unit + e2e for critical scenarios.
- Documentation and training: instructions for content managers.
More on synchronization via storage event
The storage event fires in other tabs when localStorage changes. Zustand persist automatically subscribes to this event if the storageEventListener option is enabled. We enable it by default.
What's Included
- Development and integration of the comparison component (buttons, bar, page)
- State storage configuration (Zustand + localStorage)
- Implementation of difference highlighting and best choice
- Creation of API for exporting specifications
- Testing and debugging on all devices
- Documentation for developers and content managers
- Team training on using the feature
- Technical support for 2 months after launch
Timeline
| Stage | Duration |
|---|---|
| Button + localStorage + floating bar | 3–5 working days |
| Comparison page with table and highlighting | 1–2 weeks |
| Extended version with best choice and analytics | 2–3 weeks |
Timelines vary depending on integration complexity. Contact us to discuss your project and get a free engineer consultation. We'll help select the optimal solution for your budget—the base package starts from $1,000, and additional options are calculated individually.







