Loading...
Loading...
Let shoppers select multiple products and compare them side-by-side in a table with highlighted differences to help them make the right buying decision
npx skill4agent add finsilabs/awesome-ecommerce-skills product-comparison| Platform | Recommended Approach | Why |
|---|---|---|
| Shopify | Install Comparify or Product Compare app (both free tiers available) | Shopify doesn't have built-in comparison; these apps add Compare buttons to product cards, a floating comparison tray, and a full comparison table page; they pull product metafields for spec data |
| WooCommerce | Install YITH WooCommerce Compare (free) or WooCommerce Products Compare | YITH Compare is the most popular free option — adds Compare checkboxes to product cards, a comparison table page using WooCommerce product attributes, and a "show differences only" toggle |
| BigCommerce | Enable the built-in Compare feature in Storefront → My Themes → Customize (Cornerstone theme) | BigCommerce and Cornerstone include native product comparison — enable it in the Theme Editor; it uses product custom fields as comparison attributes |
| Custom / Headless | Build a URL-state comparison tray + comparison table page with your product attribute data | Full control over attribute display, difference highlighting, and table layout; see implementation below |
// ComparisonTray.jsx
export function ComparisonTray({ selectedProducts, onRemove, onClear }) {
if (selectedProducts.length === 0) return null;
const compareUrl = `/compare?${selectedProducts.map(p => `compare=${p.id}`).join('&')}`;
return (
<div className="comparison-tray" aria-live="polite" aria-label="Products selected for comparison">
<div className="tray-products">
{selectedProducts.map(product => (
<div key={product.id} className="tray-product">
<img src={product.image} alt={product.name} width="48" height="48" />
<button onClick={() => onRemove(product.id)} aria-label={`Remove ${product.name} from comparison`}>×</button>
</div>
))}
{Array.from({ length: Math.max(0, 4 - selectedProducts.length) }).map((_, i) => (
<div key={`empty-${i}`} className="tray-placeholder" aria-hidden="true">+</div>
))}
</div>
<div className="tray-actions">
<a href={compareUrl} className="btn-primary" aria-disabled={selectedProducts.length < 2}>
Compare ({selectedProducts.length})
</a>
<button onClick={onClear}>Clear all</button>
</div>
</div>
);
}export function ProductComparisonTable({ products, attributeGroups, showOnlyDifferences }) {
function isRowIdentical(attrKey) {
const values = products.map(p => p.attributes[attrKey]);
return values.every(v => v === values[0]);
}
return (
<div className="comparison-wrapper" style={{ overflowX: 'auto' }}>
<table className="comparison-table">
<caption className="sr-only">
Side-by-side comparison of {products.map(p => p.name).join(', ')}
</caption>
<thead>
<tr>
<th scope="col" className="attr-col">Attribute</th>
{products.map(product => (
<th key={product.id} scope="col">
<img src={product.image} alt={product.name} width="80" height="80" />
<a href={product.url}>{product.name}</a>
<strong>${product.price}</strong>
<button className="btn-primary">Add to Cart</button>
</th>
))}
</tr>
</thead>
<tbody>
{attributeGroups.map(group => (
<>
<tr key={`group-${group.label}`}>
<th scope="rowgroup" colSpan={products.length + 1}>{group.label}</th>
</tr>
{group.attributes.map(attrKey => {
if (showOnlyDifferences && isRowIdentical(attrKey)) return null;
return (
<tr key={attrKey} className={isRowIdentical(attrKey) ? 'identical-row' : 'different-row'}>
<th scope="row">{attrKey.replace(/_/g, ' ')}</th>
{products.map(p => (
<td key={p.id}>{p.attributes[attrKey] ?? 'N/A'}</td>
))}
</tr>
);
})}
</>
))}
</tbody>
</table>
</div>
);
}replaceStatefunction toggleCompare(productId) {
const params = new URLSearchParams(window.location.search);
const current = params.getAll('compare');
if (current.includes(productId)) {
params.delete('compare');
current.filter(id => id !== productId).forEach(id => params.append('compare', id));
} else if (current.length < 4) {
params.append('compare', productId);
}
window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`);
}overflow-x: auto| Problem | Solution |
|---|---|
| Table overflows on mobile | Wrap in a scrollable container; use |
| Attributes missing for some products | Use "N/A" as the value — never skip the cell as it breaks column alignment |
| Comparison tray covers page content | Add |
| Products have different attribute sets | Normalize attribute keys across all compared products; fill missing values with |