civil-and-structural-engineering
Creating a Dynamic Product Comparison Table with Javascript and Css
Table of Contents
Why a Dynamic Product Comparison Table Matters for E‑Commerce
Online shoppers often struggle to decide between similar products. A static table that lists every product side by side does not help when the catalog grows to dozens of items. A dynamic product comparison table solves this by letting users choose which products to compare, filtering columns in real time, and highlighting differences. This interactivity increases engagement, reduces bounce rate, and can directly improve conversion rates. Building such a table with vanilla JavaScript and CSS is not only lightweight but also gives you full control over behavior and styling without depending on heavy libraries.
In this guide you will learn to construct a fully functional dynamic comparison table from scratch. We will cover the HTML skeleton, responsive CSS techniques, and JavaScript logic that updates the table as users interact. By the end you will have a reusable component you can adapt to any product catalog.
Core Design Principles
The table must satisfy three goals: clarity, responsiveness, and performance. Every feature row should be readable on mobile, the column count should adapt to screen width, and JavaScript operations must not block the main thread. We will achieve this by using semantic HTML, CSS Grid or Flexbox for layout, and efficient DOM manipulation.
A well‑designed comparison table also respects accessibility standards. Screen readers must be able to navigate the table logically, and interactive controls (checkboxes, dropdowns) need proper ARIA attributes. We will touch on these points throughout the implementation.
Prerequisites
Before we begin, ensure you have a basic understanding of HTML, CSS, and JavaScript. You will need a modern browser for testing (Chrome, Firefox, or Edge). No external libraries are required – everything will be built using native web APIs.
Step 1: Structuring the HTML with Data Attributes
The foundation is a <table> element with a <thead> for feature names and a <tbody> for product rows. Instead of hard‑coding product columns, we will generate them from a JavaScript array. This approach makes it trivial to add or remove products later.
Start with a minimal container and a placeholder table. We will inject the columns and rows dynamically.
<div id="comparison-app">
<div id="controls">
<fieldset>
<legend>Products to compare</legend>
<!-- Checkboxes will be inserted here -->
</fieldset>
</div>
<table id="comparisonTable">
<thead></thead>
<tbody></tbody>
</table>
</div>
Each product object in our data source will contain an id, a name, and a map of feature values. Features are defined externally so you can reuse them across different product categories.
const featureLabels = {
price: 'Price',
brand: 'Brand',
weight: 'Weight (g)',
battery: 'Battery life (hrs)',
color: 'Available colors'
};
const products = [
{ id: 1, name: 'SmartWidget Pro', price: 249, brand: 'TechCo', weight: 120, battery: 18, color: 'Black, Silver' },
{ id: 2, name: 'GadgetMax 3000', price: 189, brand: 'GadgetCorp', weight: 95, battery: 22, color: 'White, Blue' },
{ id: 3, name: 'EcoMini', price: 149, brand: 'GreenTech', weight: 80, battery: 14, color: 'Green, Beige' },
{ id: 4, name: 'TurboCharge X', price: 299, brand: 'PowerInc', weight: 150, battery: 30, color: 'Red, Black' }
];
The JavaScript will iterate over featureLabels to build the table header and rows. Each product column will be assigned a class like product-1 so we can show/hide columns easily.
Step 2: Generating the Table Dynamically
We will write a buildTable() function that creates the <thead> and <tbody> from the data. This function should be called once on page load and again if the product list changes (e.g., from an API).
function buildTable() {
const thead = document.querySelector('#comparisonTable thead');
const tbody = document.querySelector('#comparisonTable tbody');
thead.innerHTML = '';
tbody.innerHTML = '';
// Build header row
let headerRow = '<tr><th>Feature</th>';
products.forEach(p => {
headerRow += `<th class="product-${p.id}">${p.name}</th>`;
});
headerRow += '</tr>';
thead.innerHTML = headerRow;
// Build feature rows
Object.keys(featureLabels).forEach(feature => {
let row = `<tr><td><strong>${featureLabels[feature]}</strong></td>`;
products.forEach(p => {
row += `<td class="product-${p.id}">${p[feature]}</td>`;
});
row += '</tr>';
tbody.innerHTML += row;
});
}
This approach keeps the HTML concise and makes it easy to re‑render the table when needed. Note that each `
Creating the Checkbox Controls
The user needs a way to toggle product columns. We will generate a checkbox for each product inside the <fieldset>. Each checkbox has a data-product-id attribute that matches the class used in the table.
function buildControls() {
const fieldset = document.querySelector('#controls fieldset');
fieldset.innerHTML = '';
products.forEach(p => {
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.dataset.productId = p.id;
checkbox.checked = true; // shown by default
checkbox.addEventListener('change', updateVisibility);
label.appendChild(checkbox);
label.appendChild(document.createTextNode(p.name));
fieldset.appendChild(label);
});
}
Step 3: JavaScript Interactivity – Toggle Columns
The updateVisibility() function hides or shows all cells with the corresponding class. We query all elements that match `.product-${productId}` and set their display property.
function updateVisibility() {
const checkboxes = document.querySelectorAll('#controls input[type="checkbox"]');
checkboxes.forEach(cb => {
const productId = cb.dataset.productId;
const cells = document.querySelectorAll(`.product-${productId}`);
const display = cb.checked ? '' : 'none';
cells.forEach(cell => { cell.style.display = display; });
});
// Optional: remove empty columns for better aesthetics
removeEmptyColumns();
}
function removeEmptyColumns() {
// If a column is completely hidden, you can collapse it visually.
// For simplicity, this step is omitted here but can be added as an enhancement.
}
Performance note: When you toggle many columns at once, querying each class individually is efficient enough for a few dozen products. For hundreds of products, consider batching DOM updates with a DocumentFragment or using CSS classes instead of inline styles.
Step 4: Responsive CSS Styling
The table must look good on all screen sizes. We start with a base style and add media queries to handle narrow viewports.
#comparison-app {
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
th, td {
border: 1px solid #e0e0e0;
padding: 0.75rem;
text-align: left;
vertical-align: top;
}
th {
background-color: #f5f5f5;
font-weight: 600;
}
tr:nth-child(even) td {
background-color: #fafafa;
}
tr:hover td {
background-color: #f0f0f0;
}
/* Highlight differences */
tr td:first-child { font-weight: 700; }
tr td:not(:first-child):not(.same) { background-color: #fff3cd; } /* yellow for different values */
/* Responsive: stack columns on small screens */
@media (max-width: 768px) {
table, thead, tbody, th, td, tr {
display: block;
}
thead tr {
position: absolute;
top: -9999px;
left: -9999px;
}
tr {
margin-bottom: 1rem;
border: 1px solid #ccc;
}
td {
border: none;
border-bottom: 1px solid #eee;
padding-left: 50%;
position: relative;
}
td::before {
content: attr(data-label);
position: absolute;
left: 0.75rem;
width: 45%;
font-weight: 700;
white-space: nowrap;
}
/* Hide hidden columns on mobile */
td[style*="display: none"] {
display: none !important;
}
}
The mobile version uses a card‑like layout where each product becomes a block. We add data-label attributes to each cell in the JavaScript to show the feature name on the left. Below is the modification to buildTable to include the label:
Object.keys(featureLabels).forEach(feature => {
let row = `<tr><td><strong>${featureLabels[feature]}</strong></td>`;
products.forEach(p => {
row += `<td class="product-${p.id}" data-label="${p.name}">${p[feature]}</td>`;
});
row += '</tr>';
tbody.innerHTML += row;
});
Step 5: Advanced Features – Highlighting Differences and Sorting
A dynamic comparison table becomes truly powerful when it automatically highlights which features differ across selected products. For example, if three products have the same price, that row should not be highlighted. We can add a highlightDifferences() function that runs after every visibility update.
function highlightDifferences() {
const rows = document.querySelectorAll('#comparisonTable tbody tr');
const visibleCheckboxes = document.querySelectorAll('#controls input[type="checkbox"]:checked');
rows.forEach(row => {
const cells = row.querySelectorAll('td:not(:first-child)');
const visibleCells = [];
cells.forEach(cell => {
if (cell.style.display !== 'none') {
visibleCells.push(cell);
}
});
// If fewer than 2 visible columns, skip
if (visibleCells.length < 2) return;
const firstValue = visibleCells[0].textContent.trim();
let allSame = true;
for (let i = 1; i < visibleCells.length; i++) {
if (visibleCells[i].textContent.trim() !== firstValue) {
allSame = false;
break;
}
}
visibleCells.forEach(cell => {
cell.classList.toggle('same', allSame);
});
});
}
Call this function inside updateVisibility() after toggling columns. The CSS class .same can reset the background to default, while non‑same cells keep the yellow highlight defined earlier.
Sorting Products
You can also let users reorder products by a feature, such as price or battery life. Add a sort dropdown that triggers a sorting function, then rebuild the table. For clarity we will not implement the full code here, but the pattern is:
- Listen for a change on a `
- Sort the
productsarray based on the chosen feature. - Call
buildTable()andbuildControls()again, preserving checkbox states.
Re‑rendering the table is acceptable because product counts are typically under 50. For larger sets, use virtual scrolling or pagination.
Step 6: Integrating with Real Data (API Fetch)
In a production application, the product data would come from a server. We can replace the static array with a fetch() call inside an async function. Be sure to handle loading and error states.
async function loadProducts() {
try {
const response = await fetch('https://api.example.com/products');
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
products = data.products; // assume structure matches
featureLabels = data.featureLabels;
buildControls();
buildTable();
updateVisibility();
} catch (error) {
document.querySelector('#comparison-app').innerHTML = '<p>Unable to load product data. Please try again later.</p>';
console.error(error);
}
}
loadProducts();
Always provide a fallback message when data cannot load. This builds trust and improves user experience.
Step 7: Accessibility and ARIA
To make the table usable by screen readers:
- Use
<caption>to describe the table’s purpose:<caption>Product comparison – select products with checkboxes</caption>. - Add
aria-live="polite"to a container that announces when columns are shown/hidden. - Wrap product names inside
<th>withscope="col". - Use
role="checkbox"on custom checkbox controls only if not using native inputs.
Example caption insertion in buildTable():
const table = document.querySelector('#comparisonTable');
const caption = document.createElement('caption');
caption.textContent = 'Product comparison – select products with checkboxes';
table.prepend(caption);
Performance and Edge Cases
When dealing with dozens of products and many features, consider these optimizations:
- Use
requestAnimationFrameor a debounce when the user toggles many checkboxes quickly. - Instead of setting inline styles for hiding, toggle a CSS class (e.g.,
.hidden) and define.hidden { display: none !important; }. This makes it easier to override in responsive layouts. - For very large datasets, build the table only for the visible columns – virtualize rows that are off‑screen.
- Always validate product data to avoid “undefined” appearing in cells.
If a product lacks a certain feature, you can display “—” or “N/A”. Modify the template to check p[feature] ?? '—'.
Testing the Dynamic Table
Test across different devices and browsers. Use the following checklist:
- Selecting a checkbox shows/hides the corresponding column.
- Resizing the viewport triggers the responsive layout correctly.
- Highlighting differences updates when columns are toggled.
- Keyboard navigation works: tab through checkboxes, press space to toggle.
- Screen reader announces column changes (use
aria-liveregion).
For automated testing, consider writing unit tests for the sorting and highlighting logic. End‑to‑end tests can be written with Cypress or Playwright.
Conclusion and Next Steps
You now have a production‑ready dynamic product comparison table built entirely with vanilla JavaScript and CSS. This component loads data from a static array or a remote API, lets users select which products to compare, highlights differences, and adapts to any screen size. The code is modular, easy to extend, and respects accessibility best practices.
To take it further, consider adding:
- Rating stars and user review excerpts.
- Price fetch from a live source with currency formatting.
- Memory of user selections using localStorage so the chosen products persist across page visits.
- Animation when columns appear/disappear (CSS transitions on
opacityandwidth).
By mastering this pattern, you can build any interactive data table – from subscription plan comparisons to feature matrices for SaaS products. The same principles apply when working with frameworks like React or Vue, but the vanilla version gives you deeper understanding of the DOM.
For further reading, refer to the MDN documentation on HTML tables and CSS-Tricks’ roundup of responsive table patterns. These resources provide additional strategies for handling complex tabular data on the web.