
Product Comparison
- 264 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Build side-by-side product comparison tables and selectors that help shoppers evaluate specs, price, and variants, increasing confidence and reducing bounce on consideration-heavy catalogs.
About
Product-comparison from awesome-ecommerce-skills helps implement shopper-facing comparison experiences: pick products, align attributes, highlight differences, and keep CTAs visible. It targets consideration-heavy catalogs where clear spec contrast reduces abandonment.
- Designs multi-product spec matrices and diff highlights
- Handles variant-aware attribute alignment
- Implements add-to-cart actions from comparison rows
- Optimizes mobile stacking for long attribute lists
- Improves consideration-stage conversion on dense catalogs
Product Comparison by the numbers
- 264 all-time installs (skills.sh)
- Ranked #788 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill product-comparisonAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 264 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Build side-by-side product comparison tables and selectors that help shoppers evaluate specs, price, and variants, increasing confidence and reducing bounce on consideration-heavy catalogs.
Files
Product Comparison
Overview
Build a side-by-side product comparison feature where shoppers select 2–4 products and see their attributes in a sticky-header table. Attribute rows that are identical across all selected products can be hidden to reduce noise. The comparison state is stored in the URL so it can be shared or bookmarked.
When to Use This Skill
- When selling products with many technical specifications (electronics, appliances, cameras)
- When conversion research shows shoppers are considering multiple products before purchasing
- When the product catalog has well-structured attribute data that lends itself to comparison
- When building a B2B store where buyers need to justify purchase decisions to stakeholders
- When implementing a "Compare" checkbox on product listing pages
Core Instructions
Step 1: Determine the merchant's platform and choose the right approach
| 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 |
Step 2: Enable and configure comparison on your platform
---
Shopify
Using Comparify app: 1. Install Comparify – Product Comparison from the Shopify App Store (free tier available) 2. In the app settings, select which product metafields to display as comparison rows (e.g., material, weight, dimensions, warranty)
- If you haven't set up metafields yet: go to Settings → Custom data → Products and create metafields for your specs
3. The app automatically adds a "Compare" checkbox to product cards in your collection pages 4. Shoppers select up to 4 products, click "Compare" in the floating tray, and land on a full comparison table 5. Configure which attributes appear as rows and in what order in the app's Table Settings
Preparing your product data:
- Add spec data as product metafields (recommended) or in the product description with consistent formatting
- For variant specs, add them as variant metafields (e.g., weight per size)
---
WooCommerce
Using YITH WooCommerce Compare (free): 1. Install and activate from WordPress.org 2. Go to YITH → Compare → Settings 3. Under Fields to compare, select which WooCommerce product attributes and custom fields to show as rows (e.g., Color, Material, Dimensions, Weight) 4. Set Maximum products to 4 5. Enable Highlight differences to visually call out rows where products differ 6. The plugin adds a "Compare" button to product cards on your shop page and archives 7. A floating comparison bar appears at the bottom of the page as shoppers add products
Preparing your product data:
- Add comparison attributes via Products → Attributes — create attributes like "Material", "Warranty", "Compatible with" and assign values to each product
- Products must use the same attribute names for comparison rows to align correctly
---
BigCommerce
Built-in comparison (Cornerstone theme): 1. Go to Storefront → My Themes → Customize 2. Navigate to Global → Product Compare (or Category Page → Product Compare depending on your theme version) 3. Toggle Enable product comparison to On 4. Set the Maximum products (default is 4) 5. Configure which Product Custom Fields appear as comparison rows in Products → Product Custom Fields settings 6. Shoppers see a "Compare" checkbox on product cards; the floating compare tray appears automatically
Adding spec data:
- Go to Products → [product] → Custom Fields and add name/value pairs (e.g., "Screen Size: 15.6 inches", "Battery Life: 10 hours")
- Use the same field names across comparable products so they align in the comparison table
---
Custom / Headless
Comparison tray (floating bar as products are selected):
// 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>
);
}Comparison table with sticky headers and difference highlighting:
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>
);
}URL state for comparison (use `replaceState` to avoid polluting back-button history):
function 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()}`);
}Best Practices
- Limit comparison to 2–4 products — more than 4 columns breaks table layout on most screens; enforce this in the UI
- Group attributes by category — organize specs into groups (Display, Performance, Battery) to prevent a 50-row flat table
- Offer "show differences only" toggle — rows where all products share the same value add noise; provide an easy toggle
- Make the table horizontally scrollable on mobile — use
overflow-x: autoon a wrapper; never hide columns to fit small screens - Pre-populate from listing page — when a shopper clicks "Compare Now" after selecting items on the PLP, navigate with IDs in the URL
- Use consistent attribute naming — specs across products must use identical field names (e.g., "Screen Size" not sometimes "Display Size") for table rows to align
Common Pitfalls
| Problem | Solution |
|---|---|
| Table overflows on mobile | Wrap in a scrollable container; use position: sticky for the first column (attribute labels) |
| 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 padding-bottom to the page body equal to the tray height when the tray is visible |
| Products have different attribute sets | Normalize attribute keys across all compared products; fill missing values with null |
Related Skills
- @product-page-design
- @faceted-navigation
- @accessibility-commerce
- @recently-viewed-products
{
"context": "Tests whether the agent builds an accessible comparison table with grouped attributes, identical/different row differentiation, a showOnlyDifferences filter, winner highlighting, and correct CSS for sticky elements and row styling.",
"type": "weighted_checklist",
"checklist": [
{
"name": "ARIA region wrapper",
"max_score": 5,
"description": "The comparison table wrapper element has role=\"region\" and aria-label=\"Product comparison\" (or equivalent descriptive label)"
},
{
"name": "sr-only caption",
"max_score": 5,
"description": "The <table> contains a <caption> element with class \"sr-only\" (or visually-hidden equivalent) listing the compared product names"
},
{
"name": "Column scope attributes",
"max_score": 7,
"description": "Product column <th> elements have scope=\"col\" and the attribute label <th> also uses scope=\"col\" or scope=\"row\" as appropriate"
},
{
"name": "Row group scope",
"max_score": 7,
"description": "Group header <th> elements (Display, Sensor, etc.) use scope=\"rowgroup\""
},
{
"name": "Attribute row scope",
"max_score": 7,
"description": "Individual attribute label <th> elements use scope=\"row\""
},
{
"name": "Attribute grouping",
"max_score": 8,
"description": "Attributes are rendered in named groups (e.g. Display, Sensor) with a group header row separating each category"
},
{
"name": "identical-row class",
"max_score": 7,
"description": "Rows where all compared products share the same attribute value receive a CSS class indicating they are identical (e.g. \"identical-row\")"
},
{
"name": "different-row class",
"max_score": 7,
"description": "Rows where compared products have differing attribute values receive a CSS class indicating difference (e.g. \"different-row\")"
},
{
"name": "Identical row muted style",
"max_score": 4,
"description": "Identical rows are styled with a muted/grey text color (e.g. #94a3b8 or similar) applied to their <td> cells"
},
{
"name": "Different row background",
"max_score": 5,
"description": "Different-value rows have a distinct background color applied (e.g. #f8fafc or similar light shade)"
},
{
"name": "showOnlyDifferences toggle",
"max_score": 8,
"description": "When showOnlyDifferences is true, rows where all products share the same value are hidden/not rendered"
},
{
"name": "AttributeValue check/cross",
"max_score": 8,
"description": "Boolean or Yes/No attribute values are rendered as a checkmark symbol for truthy and a cross symbol for falsy/null, rather than raw true/false text"
},
{
"name": "Winner highlighting",
"max_score": 7,
"description": "Numeric attribute values are compared and the best value in each row is visually highlighted (bold, color, or class) to indicate the winner"
},
{
"name": "Sticky header row CSS",
"max_score": 7,
"description": "The CSS makes the thead row sticky at the top of the viewport using position: sticky; top: 0 (with a z-index higher than the sticky column)"
},
{
"name": "Sticky first column CSS",
"max_score": 8,
"description": "The CSS makes the attribute label column sticky to the left using position: sticky; left: 0 (NOT position: fixed)"
}
]
}
Camera Comparison Table
Problem/Feature Description
A consumer electronics retailer sells a large range of digital cameras and wants shoppers to be able to compare models side by side. The product team has noticed that customers frequently open multiple product pages in separate tabs to manually compare specs, leading to drop-off before purchase. The goal is to build a self-contained React comparison table component that can be dropped into the existing store.
The catalog team has organised camera specifications into logical groups — Display, Sensor, Performance, and Connectivity — and the data layer will supply products already normalised to a common attribute map. The component must work with a screen reader and be keyboard-accessible so the feature passes the store's WCAG compliance review. A product manager has also asked for a toggle that lets shoppers hide rows where every camera has the same value, to cut noise from a potentially 40-row spec sheet.
Output Specification
Produce the following files:
ProductComparisonTable.jsx— the main comparison table React component. Acceptproducts,attributeGroups, andshowOnlyDifferencesas props.ComparisonTable.css— all CSS for the table layout, sticky elements, and row styling.README.md— a short explanation of the component's props and how to integrate it.
The component should be runnable in isolation (no external dependencies beyond React). Use the sample data below to demonstrate the component rendering by exporting a Demo component in ProductComparisonTable.jsx that renders three cameras from the sample data.
Input Files
The following sample data is provided as context. Extract the JSON before beginning.
=============== FILE: inputs/sample-products.json =============== [ { "id": "cam-001", "name": "AlphaShot X100", "price": 899, "image": "/images/x100.jpg", "url": "/products/alphashot-x100", "attributes": { "screen_size": "3.0 inch", "resolution": "24 MP", "refresh_rate": "60 Hz", "sensor_type": "APS-C", "iso_range": "100-51200", "burst_speed": "10 fps", "video_4k": true, "wifi": true, "bluetooth": false, "weather_sealed": true } }, { "id": "cam-002", "name": "ZenLens Pro", "price": 1199, "image": "/images/zenlens.jpg", "url": "/products/zenlens-pro", "attributes": { "screen_size": "3.2 inch", "resolution": "33 MP", "refresh_rate": "60 Hz", "sensor_type": "Full Frame", "iso_range": "50-204800", "burst_speed": "15 fps", "video_4k": true, "wifi": true, "bluetooth": true, "weather_sealed": true } }, { "id": "cam-003", "name": "SnapMaster 500", "price": 499, "image": "/images/snap500.jpg", "url": "/products/snapmaster-500", "attributes": { "screen_size": "2.7 inch", "resolution": "20 MP", "refresh_rate": "60 Hz", "sensor_type": "Micro 4/3", "iso_range": "100-25600", "burst_speed": "6 fps", "video_4k": false, "wifi": true, "bluetooth": false, "weather_sealed": false } } ]
=============== FILE: inputs/attribute-groups.json =============== [ { "label": "Display", "attributes": ["screen_size", "refresh_rate"] }, { "label": "Sensor", "attributes": ["resolution", "sensor_type", "iso_range"] }, { "label": "Performance", "attributes": ["burst_speed", "video_4k"] }, { "label": "Connectivity", "attributes": ["wifi", "bluetooth", "weather_sealed"] } ]
{
"context": "Tests whether the agent correctly implements the product card compare checkbox (with limit enforcement), the floating comparison tray (with placeholder slots, count display, aria attributes, and body padding), and the correct URL structure for navigating to the comparison page.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Checkbox disabled at limit",
"max_score": 10,
"description": "The compare checkbox on a ProductCard is disabled when the product is NOT already selected AND the selection has already reached the maximum allowed (4 products)"
},
{
"name": "Maximum 4 products enforced",
"max_score": 8,
"description": "The toggle logic prevents adding more than 4 products to the comparison (the cap is 4, not 3 or 5)"
},
{
"name": "Tray hidden when empty",
"max_score": 7,
"description": "The ComparisonTray returns null (or is not rendered) when selectedProducts is empty (length === 0)"
},
{
"name": "Placeholder slots",
"max_score": 9,
"description": "The tray renders placeholder/empty slot elements for the remaining unfilled positions up to the maximum (e.g. if 2 selected, 2 placeholders are shown)"
},
{
"name": "aria-hidden on placeholders",
"max_score": 6,
"description": "The placeholder slot elements have aria-hidden=\"true\""
},
{
"name": "Count display",
"max_score": 7,
"description": "The tray displays a count in the format \"N/4 selected\" (or equivalent) showing how many products are currently selected out of the maximum"
},
{
"name": "aria-live polite",
"max_score": 8,
"description": "The tray container element has aria-live=\"polite\" to announce changes to screen reader users"
},
{
"name": "Compare link aria-disabled",
"max_score": 8,
"description": "The Compare button or link in the tray uses aria-disabled=\"true\" (or equivalent) when fewer than 2 products are selected"
},
{
"name": "Compare URL format",
"max_score": 9,
"description": "The Compare link navigates to a URL using the `compare` query parameter for each product id, e.g. /compare?compare=id1&compare=id2"
},
{
"name": "Individual product removal",
"max_score": 7,
"description": "Each product in the tray has a remove button that triggers onRemove with that product's id; the aria-label includes the product name"
},
{
"name": "Clear all button",
"max_score": 6,
"description": "The tray includes a Clear all button that triggers onClear to remove all selected products"
},
{
"name": "Body padding-bottom",
"max_score": 8,
"description": "The page or container element adds padding-bottom when the comparison tray is visible, so the tray does not obscure page content beneath it"
},
{
"name": "Checkbox label wraps input",
"max_score": 7,
"description": "The compare checkbox is wrapped in a <label> element (making the full label area clickable), and the input has an id linked to the label"
}
]
}
Product Listing Compare Selection UI
Problem/Feature Description
A home appliances e-commerce store is adding a product comparison feature to its category pages. Shoppers browsing washing machines or refrigerators need to be able to tag multiple models for side-by-side review without leaving the listing page. The UX team has designed a two-part interaction: a checkbox on each product card that the shopper ticks to include a model, and a persistent bar that floats at the bottom of the screen showing which models are currently queued for comparison.
The engineering team needs React components for both the product card (with the comparison checkbox) and the floating tray. There is a hard constraint that the comparison must accommodate between 1 and a maximum number of products — the exact limit will be enforced in the UI itself so shoppers can never accidentally add more than allowed. The tray should give shoppers clear feedback about how many slots remain, offer a direct route to the comparison page, and let them remove individual items or wipe the entire selection.
Output Specification
Produce the following files:
ProductCard.jsx— product card component that acceptsproduct,comparedIds, andonToggleCompareas props. Demonstrate it with a static list of three appliances.ComparisonTray.jsx— floating tray component that acceptsselectedProducts,onRemove, andonClearas props.ProductListing.jsx— a parent component that wiresProductCardinstances and theComparisonTraytogether, managing local state for selected product IDs. It should also render a Compare button/link that targets the comparison page with the current selection encoded in the URL.tray.css— CSS for the floating tray layout and positioning.
Use the sample product data below to populate the demonstration.
Input Files
The following sample data is provided. Extract it before beginning.
=============== FILE: inputs/products.json =============== [ { "id": "wm-101", "name": "CleanWave 7kg", "price": 349, "image": "/img/cleanwave.jpg" }, { "id": "wm-102", "name": "SpinPro Deluxe", "price": 499, "image": "/img/spinpro.jpg" }, { "id": "wm-103", "name": "EcoWash Slim", "price": 279, "image": "/img/ecowash.jpg" }, { "id": "wm-104", "name": "TurboClean X9", "price": 699, "image": "/img/turboclean.jpg" }, { "id": "wm-105", "name": "AquaFresh Plus", "price": 419, "image": "/img/aquafresh.jpg" } ]
{
"context": "Tests whether the agent correctly implements URL-based comparison state using replaceState (not pushState), normalises missing product attributes to null (not skipped), renders N/A for missing values rather than empty cells, and produces CSS with overflow-x auto, correct sticky column positioning, and min-width/border-collapse on the table.",
"type": "weighted_checklist",
"checklist": [
{
"name": "URLSearchParams usage",
"max_score": 8,
"description": "The hook reads and writes comparison IDs using URLSearchParams with 'compare' as the query parameter key (params.getAll('compare') or equivalent)"
},
{
"name": "history.replaceState not pushState",
"max_score": 12,
"description": "The hook updates the URL using window.history.replaceState (NOT history.pushState or router.push) when toggling products"
},
{
"name": "Normalize all attribute keys",
"max_score": 8,
"description": "getComparisonData collects the union of all attribute keys across all fetched products before building each product's attribute map"
},
{
"name": "null for missing attributes",
"max_score": 10,
"description": "Attributes absent for a given product are represented as null in that product's attribute map (not omitted/undefined)"
},
{
"name": "N/A rendered for null",
"max_score": 10,
"description": "The component or data layer renders \"N/A\" (or equivalent fallback text) for null attribute values rather than leaving the cell empty or skipping it"
},
{
"name": "overflow-x auto on wrapper",
"max_score": 9,
"description": "The CSS applies overflow-x: auto to the table wrapper element (and optionally -webkit-overflow-scrolling: touch)"
},
{
"name": "Sticky first column with position:sticky",
"max_score": 11,
"description": "The CSS makes the attribute label column sticky using position: sticky and left: 0 — NOT position: fixed"
},
{
"name": "Sticky column background and z-index",
"max_score": 7,
"description": "The sticky attribute label column has an explicit background-color and a z-index set (to prevent content showing through when scrolling)"
},
{
"name": "Table min-width",
"max_score": 7,
"description": "The comparison table element has min-width: 600px (or a similar minimum that prevents premature column collapse)"
},
{
"name": "border-collapse: collapse",
"max_score": 6,
"description": "The comparison table CSS includes border-collapse: collapse"
},
{
"name": "Architecture doc documents replaceState reason",
"max_score": 6,
"description": "ARCHITECTURE.md mentions the reason for using replaceState over pushState (back-button history / navigation behaviour)"
},
{
"name": "Architecture doc documents null handling",
"max_score": 6,
"description": "ARCHITECTURE.md mentions the approach for handling missing attributes (null value, N/A display, column alignment rationale)"
}
]
}
Comparison State Hook and Data API
Problem/Feature Description
A furniture retailer is launching a product comparison feature. The frontend team needs two things: a reusable React hook that keeps the list of selected product IDs in sync with the URL so shoppers can share or bookmark their comparison, and a server-side data function that fetches and normalises product records before the comparison table renders them.
The team has run into a specific issue in a previous prototype: navigating back and forward in the browser produced unexpected behaviour because every checkbox click added a new entry to browser history. The new hook must avoid this. Another problem in the prototype was that some furniture pieces do not have every specification filled in — the previous version silently dropped those cells, which misaligned table columns. The new data layer must handle absent attributes in a way that keeps the table structurally consistent.
The team also wants a complete CSS file for the comparison table so that it scrolls correctly on small screens and keeps the row labels visible while scrolling horizontally on larger screens.
Output Specification
Produce the following files:
hooks/useProductComparison.js— the URL-syncing comparison hook. ExportuseProductComparisonwhich returns{ comparedIds, toggle, clearAll }.api/comparison.js— the data-fetching function. ExportgetComparisonData(productIds)that returns normalised product objects. Use the mock data below instead of a real database call.ComparisonTable.css— CSS for the table wrapper and column/row layout, including mobile scrolling behaviour and sticky positioning.ARCHITECTURE.md— a brief document (bullet points are fine) describing the key design decisions made in the hook and the data function, especially around URL management and missing attribute handling.
Input Files
The following mock product database is provided. Extract it before beginning.
=============== FILE: inputs/mock-db.json =============== [ { "id": "sofa-001", "name": "CloudComfort 3-Seater", "slug": "cloudcomfort-3-seater", "price": 1299, "images": ["/img/cc3.jpg"], "attributes": [ { "key": "material", "value": "Linen" }, { "key": "seat_depth", "value": "60 cm" }, { "key": "leg_material", "value": "Oak" }, { "key": "removable_covers", "value": "Yes" }, { "key": "weight_kg", "value": "45" } ], "variants": [{ "price": 1299 }] }, { "id": "sofa-002", "name": "ModularLux Corner", "slug": "modularlux-corner", "price": 2199, "images": ["/img/mlcorner.jpg"], "attributes": [ { "key": "material", "value": "Velvet" }, { "key": "seat_depth", "value": "65 cm" }, { "key": "leg_material", "value": "Metal" }, { "key": "weight_kg", "value": "78" } ], "variants": [{ "price": 2199 }] }, { "id": "sofa-003", "name": "SlimLine 2-Seater", "slug": "slimline-2-seater", "price": 799, "images": ["/img/slim2.jpg"], "attributes": [ { "key": "material", "value": "Microfibre" }, { "key": "seat_depth", "value": "55 cm" }, { "key": "removable_covers", "value": "No" }, { "key": "weight_kg", "value": "32" } ], "variants": [{ "price": 799 }] } ]
{
"name": "finsi/product-comparison",
"version": "0.1.0",
"summary": "Side-by-side feature comparison tables with dynamic attribute selection",
"skills": {
"product-comparison": {
"path": "SKILL.md"
}
}
}