PHPackages                             justinholtweb/craft-compare - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. justinholtweb/craft-compare

ActiveCraft-plugin

justinholtweb/craft-compare
===========================

Side-by-side comparison for Craft CMS — let visitors compare Commerce products, entries or categories in a comparison table you define once in the control panel.

00PHP

Since Aug 23Pushed todayCompare

[ Source](https://github.com/justinholtweb/craft-compare)[ Packagist](https://packagist.org/packages/justinholtweb/craft-compare)[ RSS](/packages/justinholtweb-craft-compare/feed)WikiDiscussions main Synced today

READMEChangelogDependenciesVersions (1)Used By (0)

Compare for Craft CMS
=====================

[](#compare-for-craft-cms)

Side-by-side comparison for Craft CMS 5. Visitors build a list of products — or entries, or categories — and see them in a comparison table you define once in the control panel.

WooCommerce shops have had this for years, via plugins like WPC Smart Compare. Craft has not, and the reason those plugins do not translate is that they compare *products*: their table is a fixed list of known product properties. Craft's content is whatever you modelled it to be.

Compare's answer is that **a table row is a resolver, not a column**. A row knows how to read itself off whatever element it is handed. So one mechanism draws a laptop's `processor` and `ramGb`, a sofa's `price` and `stock`, and a case study's `industry` and `duration` — and the difference highlighting works the same way for all of them.

---

What it does
------------

[](#what-it-does)

- **Compare anything.** Commerce products and variants, entries, categories. One element type per compare set; as many sets as you need.
- **A table you define in the CP.** Drag rows into order: the title, an image, any custom field, element attributes, price, SKU, stock, an add-to-cart button, a link — or a Twig template when none of those fit.
- **Difference highlighting that means something.** Rows whose values agree are greyed out and can be hidden entirely. Comparison happens on *normalized values*, not rendered markup, so `$10.00`and `$10` are the same price and two relation fields holding the same three entries in a different order are the same answer.
- **Lists that survive.** A guest's comparison lives in the database under a cookie token, not in the cookie. Sign in and it merges into your account. Come back on your laptop and it is there.
- **A comparison bar and a modal**, both zero-dependency, both progressively enhanced from real forms that work with JavaScript switched off.
- **A real page at `/compare`** that can be bookmarked, printed and — on Pro — shared by link.
- **Insights (Pro):** which products your customers put *next to each other*. That is your competitive set as they see it, which is rarely how your catalogue is organised.

Commerce is a soft dependency. Compare works perfectly well on a content site with no shop.

---

Requirements
------------

[](#requirements)

Craft CMS5.3+PHP8.2+Craft Commerce5.0+, optional — needed only for product, price, SKU, stock and cart rowsInstallation
------------

[](#installation)

```
composer require justinholtweb/craft-compare
php craft plugin/install compare
```

---

Getting started
---------------

[](#getting-started)

### 1. Create a compare set

[](#1-create-a-compare-set)

**Settings → Compare → Sets → New compare set.**

- **Name and handle** — the handle is what templates and endpoints use.
- **What does this set compare?** — Entries, Categories, or (with Commerce) Products or Variants.
- **Which ones?** — tick the sections, category groups or product types in the set. Tick nothing to include all of them.
- **Maximum items** — how many go side by side. Three fits a phone; ten is the Pro ceiling.
- **Table rows** — the point of the screen. Add rows, choose a type, set a label, drag to reorder.

### 2. Put a compare button on your product cards

[](#2-put-a-compare-button-on-your-product-cards)

```
{{ craft.compare.button(product) }}
```

That is the whole integration. It renders a real form posting to Compare's toggle action, and the bundled runtime turns it into a fetch. Add the bar once in your layout:

```
{{ craft.compare.bar() }}
```

### 3. That is it

[](#3-that-is-it)

Visitors add things, the bar appears, "Compare (3)" opens the table. `/compare` shows the same table as a page.

---

Templating
----------

[](#templating)

Every method takes an optional set handle. With one set — every Lite install and most Pro ones — you can leave it out.

```
{# state #}
{{ craft.compare.count() }}                  {# how many are on the list #}
{{ craft.compare.has(product) }}             {# is this one on it? #}
{{ craft.compare.isFull() }}
{{ craft.compare.items() }}                  {# the elements, in list order #}
{{ craft.compare.list() }}                   {# the CompareList model #}
{{ craft.compare.url() }}                    {# the comparison page #}
{{ craft.compare.setFor(product) }}          {# which set this element belongs to #}
{{ craft.compare.sets }}

{# ready-made markup #}
{{ craft.compare.button(product) }}
{{ craft.compare.button(product, { text: 'Add to compare', addedText: 'Added', class: 'btn' }) }}
{{ craft.compare.bar() }}

{# the table #}
{{ craft.compare.render() }}                                  {# the visitor's own list #}
{{ craft.compare.render(products) }}                          {# these elements, right now #}
{{ craft.compare.render(null, { differencesOnly: true }) }}

{# or build it and draw it yourself #}
{% set table = craft.compare.table() %}
{% for row in table.getVisibleRows(true) %}

        {{ row.getLabel() }}
        {% for cell in row.cells %}
            {{ cell.isEmpty() ? '—' : cell.html }}
        {% endfor %}

{% endfor %}
```

### Rolling your own buttons

[](#rolling-your-own-buttons)

Any element with the right data attributes is picked up, including ones added to the page later by your own JavaScript:

```
Compare
0
Compare now
Clear
Remove
```

The runtime fires `compare:changed` and `compare:toggled` on `document`, and exposes `window.craftCompare.refresh()`, `.open(setHandle)` and `.state()`.

### Overriding the markup

[](#overriding-the-markup)

To replaceCreateThe comparison table`templates/compare/_table.twig`The comparison page`templates/compare/index.twig`One set's table onlyset a **Custom table template** on the setTable templates get `table`, `set`, `differencesOnly`, `settings` and `plugin`.

---

Row types
---------

[](#row-types)

RowWhat it readsNeeds**Title**The element's title, optionally linked**Image**An asset field, or the first one it finds**Custom field**Any field, rendered and normalized by type**Element attribute**Post date, expiry, created, updated, slug, ID, status, author**Link**A "View" button**Price**Cheapest variant, default variant, or a rangeCommerce**SKU**The purchasable's SKUCommerce**Stock**In stock / out of stock, or the countCommerce**Add to cart**A real Commerce add-to-cart formCommerce**Twig template**Whatever you renderProA field handle that is not on a particular element gives that cell a blank rather than an error — which is the normal case for a set spanning two entry types, not a mistake.

Plugins can register their own:

```
Event::on(Rows::class, Rows::EVENT_REGISTER_ROW_TYPES, function(RegisterComponentTypesEvent $e) {
    $e->types[] = MyRatingRow::class;
});
```

---

The JSON API
------------

[](#the-json-api)

Every endpoint the bundled runtime uses is public and documented, so you can turn off **Load the bundled front end** and drive it from your own build.

MethodEndpointBody`GET``/compare/session.json`—`POST``compare/list/add``elementId``POST``compare/list/remove``elementId``POST``compare/list/toggle``elementId``POST``compare/list/clear``set``POST``compare/list/reorder``set`, `ids[]``GET``compare/list/table``set`, optional `ids`, `differencesOnly`, `format=json``POST``compare/list/share``set` (Pro)`session.json` is the one to start with. It returns a **fresh CSRF token** and the current state of every set:

```
{
  "csrfTokenName": "CRAFT_CSRF_TOKEN",
  "csrfToken": "…",
  "loggedIn": false,
  "mayCompare": true,
  "sets": {
    "products": { "count": 2, "max": 4, "full": false, "items": [ … ] }
  }
}
```

### Why that endpoint exists

[](#why-that-endpoint-exists)

Compare buttons live on product cards, and product cards live on cached pages — `{% cache %}`, Blitz, a CDN. On a cached page the CSRF token is stale and the button does not know whether the visitor already added this product. So the runtime never trusts the HTML it was rendered into: it fetches the session, takes the token from there, and reconciles the buttons. Cached and uncached pages then behave identically.

A refusal — a full list, or "please sign in" — comes back as `200` with `success: false` and a human-readable `error`. It is an answer, not a fault.

---

Settings
--------

[](#settings)

SettingDefaultComparison page URI`compare`Must not clash with an entry URIComparison bar positionBottomOr top, or noneOpen when fullOffA modal nobody asked for is an interruptionLoad the bundled front endOnOff if you are driving the API yourselfRequire loginOffGuest list lifetime30 daysSigned-in users' lists never expireCookie name`CraftCompareToken`Holds a token, never the listRecord insightsOnPro. Element IDs only — no personal dataKeep insights for90 daysSwept by Craft's garbage collectionShare link lifetimeForeverProConsole
-------

[](#console)

```
php craft compare            # what Compare currently holds
php craft compare/prune      # drop abandoned guest lists and expired events
```

---

Editions
--------

[](#editions)

LitePro**Price**Free**$79**, then $29/yearCompare sets1UnlimitedItems per comparison3Up to 10All core row types✓✓Guest + user lists, merged on login✓✓Bar, modal, standalone page, print✓✓Twig API + JSON endpoints✓✓Custom table templates✓✓Difference highlighting and "differences only"—✓Twig template rows—✓Shareable comparison links—✓Insights—✓Lite is a complete comparator, not a demo. A shop with one catalogue and a three-up table — which is what almost every comparison table on the web actually is — never meets a paywall.

A lapsed Pro licence **narrows rather than breaks**: the table drops to three columns and the Twig rows are skipped, but the page keeps working and nothing stored is changed. Renewing brings it all back.

---

Privacy
-------

[](#privacy)

Compare stores element IDs, a random guest token, and — for signed-in visitors — a user ID. It records no IP addresses, no user agents, and makes no outbound requests of any kind. Insights are element IDs and timestamps.

Guest lists are deleted after their configured lifetime. Signed-in users' lists are their data and are deleted with their account.

Licence
-------

[](#licence)

Proprietary. See `LICENSE.md`.

###  Health Score

20

—

LowBetter than 12% of packages

Maintenance65

Regular maintenance activity

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/035cb655c55af0e9e5b96754b80fd9703e195c32dbdfc49ae9a43ab9cf8db560?d=identicon)[justinholtweb](/maintainers/justinholtweb)

---

Top Contributors

[![justinholtweb](https://avatars.githubusercontent.com/u/295903?v=4)](https://github.com/justinholtweb "justinholtweb (3 commits)")

### Embed Badge

![Health badge](/badges/justinholtweb-craft-compare/health.svg)

```
[![Health](https://phpackages.com/badges/justinholtweb-craft-compare/health.svg)](https://phpackages.com/packages/justinholtweb-craft-compare)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
