Back to Widget Patterns
v2.5Interactive Tooling
Comparison Tool
Feature comparison table: the host product against 2 to 3 competitors. Boolean, text, and rating value types. Optional category toggles and a highlighted column. Composes PrimaryButtonCTA at the footer.
Live preview
Rendered with real component code. Each example demonstrates a documented variant.
Three-way comparison
How we compare
A side-by-side look at the features that matter most.
| Our product (Our pick) | Acme Suite | Globex Pro | |
|---|---|---|---|
| Free trialNo credit card required | |||
| Custom domain | |||
| API access | Full REST + webhooks | Read-only REST | REST only |
| Support | 24/7 live chat | Email (48 h) | Tickets only |
| Ease of setup | |||
| Analytics depth |
With category toggles
How we compare
Expand each category to explore specific features.
| Our product (Our pick) | Acme Suite | Globex Pro | |
|---|---|---|---|
| Free trialNo credit card required | |||
| Custom domain | |||
React props
Configure the component via props in React or via attributes in HTML.
| Prop | Type | Default | Description |
|---|---|---|---|
| competitors | Competitor[] | required | Ordered list of column definitions; set highlighted: true on your product to apply the accent column style |
| features | FeatureRow[] | required | Ordered list of feature rows; each row carries a values map keyed by competitor name |
| title | string | undefined | Optional heading centered above the table |
| description | string | undefined | Optional supporting copy shown below the title |
| showCategoryToggles | boolean | false | When true, groups rows by the category field and renders collapsible category header rows |
| defaultExpandedCategories | string[] | [] | Category names that are expanded on initial render when showCategoryToggles is true |
| finalCta | { label: string; href: string } | undefined | Renders a centered PrimaryButtonCTA below the table when provided |
| className | string | undefined | Extra class applied to the root section element |
HTML usage
The HTML variant uses the same shared styles and class names. Drop into any stack.
<link rel="stylesheet" href="path/to/comparison-tool-vs-competitors/styles.css">
<section class="cmp">
<header class="cmp__header">
<h2 class="cmp__title">How we compare</h2>
<p class="cmp__description">A side-by-side look at the features that matter most.</p>
</header>
<div class="cmp__table-wrap">
<table class="cmp__table">
<thead>
<tr>
<th scope="col" class="cmp__col-head"></th>
<!-- Mark your product column with cmp__col-head--highlighted -->
<th scope="col" class="cmp__col-head cmp__col-head--highlighted">Our product</th>
<th scope="col" class="cmp__col-head">Acme Suite</th>
<th scope="col" class="cmp__col-head">Globex Pro</th>
</tr>
</thead>
<tbody>
<!-- Boolean row -->
<tr>
<th scope="row" class="cmp__row-head">
Free trial
<span class="cmp__row-description">No credit card required</span>
</th>
<!-- highlighted cell -->
<td class="cmp__cell cmp__cell--highlighted">
<span aria-label="Yes">
<svg class="cmp__icon" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<circle cx="10" cy="10" r="9" fill="#1e5fcf" opacity="0.12"/>
<path d="M6 10.5l3 3 5-5" stroke="#1e5fcf" stroke-width="1.75"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</td>
<td class="cmp__cell">
<span aria-label="Yes"><!-- same check SVG --></span>
</td>
<td class="cmp__cell">
<span aria-label="No">
<svg class="cmp__icon" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<circle cx="10" cy="10" r="9" fill="rgba(16,37,66,0.3)" opacity="0.08"/>
<path d="M7 7l6 6M13 7l-6 6" stroke="rgba(16,37,66,0.3)"
stroke-width="1.75" stroke-linecap="round"/>
</svg>
</span>
</td>
</tr>
<!-- Text row -->
<tr>
<th scope="row" class="cmp__row-head">API access</th>
<td class="cmp__cell cmp__cell--highlighted">Full REST + webhooks</td>
<td class="cmp__cell">Read-only REST</td>
<td class="cmp__cell">REST only</td>
</tr>
<!-- Rating row (5 pips) -->
<tr>
<th scope="row" class="cmp__row-head">Ease of setup</th>
<td class="cmp__cell cmp__cell--highlighted">
<span class="cmp__rating" aria-label="5 out of 5">
<!-- 5 filled SVG circles using cmp__rating-pip class -->
</span>
</td>
<td class="cmp__cell">
<span class="cmp__rating" aria-label="3 out of 5">
<!-- 3 filled, 2 empty pips -->
</span>
</td>
<td class="cmp__cell">
<span class="cmp__rating" aria-label="2 out of 5"><!-- 2 filled pips --></span>
</td>
</tr>
<!-- Category toggle row (used when showCategoryToggles is true) -->
<tr class="cmp__category-row">
<td colspan="4" class="cmp__category-cell">
<button type="button" class="cmp__category-toggle" aria-expanded="true">
<svg class="cmp__chevron cmp__chevron--expanded" viewBox="0 0 16 16"
width="14" height="14" fill="none" aria-hidden="true">
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Pricing
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="cmp__footer">
<a class="pbc pbc--solid pbc--rounded" href="/signup">Start free trial</a>
</div>
</section>
<script>
/* Toggle category visibility when the button is clicked. */
(function () {
document.querySelectorAll('.cmp__category-toggle').forEach(function (btn) {
btn.addEventListener('click', function () {
var expanded = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!expanded));
btn.querySelector('.cmp__chevron').classList.toggle('cmp__chevron--expanded', !expanded);
/* Show/hide the sibling rows until the next category row. */
var row = btn.closest('tr').nextElementSibling;
while (row && !row.classList.contains('cmp__category-row')) {
row.hidden = expanded;
row = row.nextElementSibling;
}
});
});
}());
</script>Customization
Override these CSS custom properties to integrate the component into your brand.
.cmp {
--cmp-bg: var(--brand-surface, #ffffff);
--cmp-cell-bg: var(--brand-surface, #ffffff);
--cmp-cell-bg-highlighted: rgba(30, 95, 207, 0.05);
--cmp-border: rgba(0, 0, 0, 0.1);
--cmp-text-color: var(--brand-ink, #102542);
--cmp-muted-color: rgba(16, 37, 66, 0.55);
--cmp-icon-check: var(--brand-accent, #1e5fcf);
--cmp-icon-x: rgba(16, 37, 66, 0.3);
--cmp-rating-filled: var(--brand-accent, #1e5fcf);
--cmp-rating-empty: rgba(16, 37, 66, 0.15);
--cmp-category-bg: rgba(0, 0, 0, 0.025);
--cmp-radius: 1rem;
--cmp-cell-padding: 0.875rem 1rem;
--cmp-section-padding: 2rem;
}