@use 'sprucecss/scss/spruce' as *;
.product-compare {
@include layout-stack('m');
&__header {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: spacer('s');
justify-content: space-between;
}
&__title {
font-size: font-size('h4');
margin: 0;
}
&__scroll {
border: config('border-width-base', $display) solid color('border');
border-radius: radius('lg');
overflow-x: auto;
&:focus-visible {
outline: 2px solid color('focus-ring');
outline-offset: 2px;
}
}
&__table {
margin: 0;
min-inline-size: 40rem;
table-layout: fixed;
th,
td {
padding: spacer('s');
vertical-align: top;
}
tbody th {
color: color('muted');
font-weight: 600;
white-space: nowrap;
}
tbody tr:nth-child(odd) {
background-color: color('surface-muted');
}
tr > :first-child {
background-color: color('surface');
inset-inline-start: 0;
position: sticky;
z-index: 1;
}
tbody tr:nth-child(odd) > :first-child {
background-color: color('surface-muted');
}
tr[data-same] td {
color: color('muted');
}
}
&__corner {
inline-size: 10rem;
}
&__product {
position: relative;
text-align: start;
img {
aspect-ratio: 1;
border-radius: radius('md');
inline-size: 7.5rem;
margin-block-end: spacer('xs');
object-fit: cover;
}
.btn {
margin-block-start: spacer('xs');
}
}
&__name {
color: color('heading');
display: block;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
&__price {
color: color('heading');
display: block;
font-weight: 400;
}
&__remove {
@include clear-btn;
background-color: color('surface');
border: config('border-width-base', $display) solid color('border');
border-radius: radius('pill');
color: color('muted');
display: inline-flex;
inset-block-start: spacer('s');
inset-inline-end: spacer('s');
padding: spacer('xxs');
position: absolute;
svg {
block-size: 0.875rem;
inline-size: 0.875rem;
}
&:hover {
color: color('danger', 'alert');
}
&:focus-visible {
outline: 2px solid color('focus-ring');
}
}
&__icon {
block-size: 1.25rem;
color: color('muted');
inline-size: 1.25rem;
&--yes {
color: color('success', 'alert');
}
}
}
<section class="product-compare" aria-labelledby="product-compare-title" data-product-compare>
<header class="product-compare__header">
<h2 class="product-compare__title" id="product-compare-title">Compare backpacks</h2>
<label class="form-switch" data-compare-differences hidden>
<input class="form-switch__control" type="checkbox" role="switch">
<span class="form-label form-switch__label">Only show differences</span>
</label>
</header>
<div class="product-compare__scroll" role="region" aria-labelledby="product-compare-title" tabindex="0">
<table class="table product-compare__table">
<caption class="sr-only">Comparison of 3 backpacks; the products are in the columns</caption>
<thead>
<tr>
<td class="product-compare__corner"></td>
<th scope="col" class="product-compare__product" data-compare-column>
<img src="https://images.unsplash.com/photo-1553062407-98eeb64c6a62?auto=format&fit=crop&w=240&h=240&q=60" alt="" width="120" height="120" loading="lazy">
<a class="product-compare__name" href="#">Everyday Backpack</a>
<span class="product-compare__price">$89</span>
<a class="btn btn--primary btn--sm" href="#">Add to cart<span class="sr-only">: Everyday Backpack</span></a>
<button class="product-compare__remove" type="button" data-compare-remove hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18M6 6l12 12"/></svg>
<span class="sr-only">Remove Everyday Backpack from the comparison</span>
</button>
</th>
<th scope="col" class="product-compare__product" data-compare-column>
<img src="https://images.unsplash.com/photo-1577733966973-d680bffd2e80?auto=format&fit=crop&w=240&h=240&q=60" alt="" width="120" height="120" loading="lazy">
<a class="product-compare__name" href="#">Heritage Backpack</a>
<span class="product-compare__price">$189</span>
<a class="btn btn--primary btn--sm" href="#">Add to cart<span class="sr-only">: Heritage Backpack</span></a>
<button class="product-compare__remove" type="button" data-compare-remove hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18M6 6l12 12"/></svg>
<span class="sr-only">Remove Heritage Backpack from the comparison</span>
</button>
</th>
<th scope="col" class="product-compare__product" data-compare-column>
<img src="https://images.unsplash.com/photo-1585916420730-d7f95e942d43?auto=format&fit=crop&w=240&h=240&q=60" alt="" width="120" height="120" loading="lazy">
<a class="product-compare__name" href="#">Commuter Pack</a>
<span class="product-compare__price">$129</span>
<a class="btn btn--primary btn--sm" href="#">Add to cart<span class="sr-only">: Commuter Pack</span></a>
<button class="product-compare__remove" type="button" data-compare-remove hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18M6 6l12 12"/></svg>
<span class="sr-only">Remove Commuter Pack from the comparison</span>
</button>
</th>
</tr>
</thead>
<tbody>
<tr data-compare-row>
<th scope="row">Rating</th>
<td>4.7 / 5</td>
<td>4.6 / 5</td>
<td>4.4 / 5</td>
</tr>
<tr data-compare-row>
<th scope="row">Capacity</th>
<td>22 L</td>
<td>26 L</td>
<td>24 L</td>
</tr>
<tr data-compare-row>
<th scope="row">Weight</th>
<td>0.9 kg</td>
<td>1.4 kg</td>
<td>1.1 kg</td>
</tr>
<tr data-compare-row>
<th scope="row">Material</th>
<td>Recycled canvas</td>
<td>Waxed cotton, leather</td>
<td>Coated nylon</td>
</tr>
<tr data-compare-row>
<th scope="row">Laptop sleeve</th>
<td>16″</td>
<td>16″</td>
<td>16″</td>
</tr>
<tr data-compare-row>
<th scope="row">Water-resistant</th>
<td><svg class="product-compare__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M5 12h14"/></svg><span class="sr-only">No</span></td>
<td><svg class="product-compare__icon product-compare__icon--yes" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20 6 9 17l-5-5"/></svg><span class="sr-only">Yes</span></td>
<td><svg class="product-compare__icon product-compare__icon--yes" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20 6 9 17l-5-5"/></svg><span class="sr-only">Yes</span></td>
</tr>
<tr data-compare-row>
<th scope="row">Lifetime repairs</th>
<td><svg class="product-compare__icon product-compare__icon--yes" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20 6 9 17l-5-5"/></svg><span class="sr-only">Yes</span></td>
<td><svg class="product-compare__icon product-compare__icon--yes" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20 6 9 17l-5-5"/></svg><span class="sr-only">Yes</span></td>
<td><svg class="product-compare__icon product-compare__icon--yes" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20 6 9 17l-5-5"/></svg><span class="sr-only">Yes</span></td>
</tr>
<tr data-compare-row>
<th scope="row">Warranty</th>
<td>2 years</td>
<td>2 years</td>
<td>5 years</td>
</tr>
</tbody>
</table>
</div>
<p class="sr-only" aria-live="polite" data-compare-live></p>
</section>
(() => {
document.querySelectorAll('[data-product-compare]').forEach((compare) => {
const table = compare.querySelector('table');
const toggle = compare.querySelector('[data-compare-differences]');
const checkbox = toggle.querySelector('input');
const live = compare.querySelector('[data-compare-live]');
const markRows = () => {
table.querySelectorAll('[data-compare-row]').forEach((row) => {
const values = [...row.querySelectorAll('td')].map((cell) => cell.textContent.trim());
row.toggleAttribute('data-same', new Set(values).size <= 1);
});
};
const filterRows = () => {
table.querySelectorAll('[data-compare-row]').forEach((row) => {
row.hidden = checkbox.checked && row.hasAttribute('data-same');
});
};
toggle.hidden = false;
checkbox.addEventListener('change', () => {
filterRows();
const hidden = table.querySelectorAll('[data-compare-row][hidden]').length;
live.textContent = checkbox.checked ? `${hidden} identical ${hidden === 1 ? 'row' : 'rows'} hidden.` : 'All rows shown.';
});
table.querySelectorAll('[data-compare-remove]').forEach((button) => {
button.hidden = false;
button.addEventListener('click', () => {
const header = button.closest('th');
const index = header.cellIndex;
const name = header.querySelector('.product-compare__name').textContent;
const columns = table.querySelectorAll('[data-compare-column]').length;
if (columns <= 2) {
live.textContent = 'Keep at least two products to compare.';
return;
}
table.querySelectorAll('tr').forEach((row) => row.cells[index].remove());
table.querySelector('caption').textContent = `Comparison of ${columns - 1} backpacks; the products are in the columns`;
markRows();
filterRows();
live.textContent = `${name} removed from the comparison.`;
table.querySelector('[data-compare-remove]').focus();
});
});
markRows();
});
})();