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 `` and `` carries a class with the product ID – this is the key to dynamic hiding.

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:

  1. Listen for a change on a `