In many projects, displaying large datasets in a clean and user-friendly way can be a challenge. Too much information on one screen can overwhelm users and make navigation difficult.
A great solution is an expandable table with collapsible rows. This allows users to view a summary at first and expand only the rows they’re interested in for more details. It’s ideal for dashboards, reports, and data-heavy applications.
In this article, I’ll show you how to build this interactive table using HTML, CSS, and JavaScript. We’ll look at a few simple approaches so you can choose the one that works best for your project.
Method 1 – Basic Expandable Table Using Pure JavaScript
Let’s start with a simple implementation that uses minimal code:
Read Python Django Format Date
Step 1: Create the HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Expandable Table</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>U.S. State Population Data</h1>
<table id="expandableTable">
<thead>
<tr>
<th></th>
<th>State</th>
<th>Population</th>
<th>Region</th>
</tr>
</thead>
<tbody>
<tr class="parent-row" data-id="1">
<td><span class="expand-btn">+</span></td>
<td>California</td>
<td>39.5M</td>
<td>West</td>
</tr>
<tr class="child-row hidden" data-parent="1">
<td colspan="4">
<div class="child-content">
<p><strong>Capital:</strong> Sacramento</p>
<p><strong>Largest City:</strong> Los Angeles</p>
<p><strong>Area:</strong> 163,696 sq mi</p>
<p><strong>Statehood:</strong> September 9, 1850</p>
</div>
</td>
</tr>
<tr class="parent-row" data-id="2">
<td><span class="expand-btn">+</span></td>
<td>Texas</td>
<td>29.1M</td>
<td>South</td>
</tr>
<tr class="child-row hidden" data-parent="2">
<td colspan="4">
<div class="child-content">
<p><strong>Capital:</strong> Austin</p>
<p><strong>Largest City:</strong> Houston</p>
<p><strong>Area:</strong> 268,596 sq mi</p>
<p><strong>Statehood:</strong> December 29, 1845</p>
</div>
</td>
</tr>
<tr class="parent-row" data-id="3">
<td><span class="expand-btn">+</span></td>
<td>Florida</td>
<td>21.5M</td>
<td>South</td>
</tr>
<tr class="child-row hidden" data-parent="3">
<td colspan="4">
<div class="child-content">
<p><strong>Capital:</strong> Tallahassee</p>
<p><strong>Largest City:</strong> Jacksonville</p>
<p><strong>Area:</strong> 65,758 sq mi</p>
<p><strong>Statehood:</strong> March 3, 1845</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<script src="script.js"></script>
</body>
</html>
You can see the output in the screenshot below.

Check out Create a Registration Form with HTML, CSS, and JavaScript
Step 2: Add CSS Styling
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f8f9fa;
margin: 0;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 30px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
font-weight: bold;
}
tr:hover {
background-color: #f5f5f5;
}
.expand-btn {
display: inline-block;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
cursor: pointer;
background-color: #e9ecef;
border-radius: 50%;
font-weight: bold;
}
.expand-btn.active {
background-color: #007bff;
color: white;
}
.child-row {
background-color: #f8f9fa;
}
.child-content {
padding: 15px;
background-color: #f8f9fa;
}
.hidden {
display: none;
}
You can see the output in the screenshot below.

Read Create Interactive HTML Forms with CSS and JavaScript
Step 3: Create the JavaScript Logic
document.addEventListener('DOMContentLoaded', function() {
const expandButtons = document.querySelectorAll('.expand-btn');
expandButtons.forEach(btn => {
btn.addEventListener('click', function() {
// Get the parent row
const parentRow = this.closest('tr');
const parentId = parentRow.getAttribute('data-id');
// Find all child rows for this parent
const childRows = document.querySelectorAll(`.child-row[data-parent="${parentId}"]`);
// Toggle the expand/collapse button
this.textContent = this.textContent === '+' ? '-' : '+';
this.classList.toggle('active');
// Toggle visibility of child rows
childRows.forEach(row => {
row.classList.toggle('hidden');
});
});
});
});
You can see the output in the screenshot below.

This creates a basic expandable table with U.S. state data. When you click the + button, additional information about each state appears below the row.
Check out Call a JavaScript Function When a Checkbox is Checked or Unchecked
Method 2 – Dynamic Data Loading with JSON
For larger datasets, it’s more efficient to load the expanded content dynamically rather than having it all in the HTML. Here’s how to implement that:
Step 1: Update the HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Expandable Table</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>U.S. State Population Data</h1>
<table id="expandableTable">
<thead>
<tr>
<th></th>
<th>State</th>
<th>Population</th>
<th>Region</th>
</tr>
</thead>
<tbody id="tableBody">
<!-- Table content will be loaded dynamically -->
</tbody>
</table>
</div>
<script src="script-dynamic.js"></script>
</body>
</html>
Read Check if a Checkbox is Checked Using jQuery
Step 2: Create a JSON Data File (states.json)
{
"states": [
{
"id": 1,
"name": "California",
"population": "39.5M",
"region": "West",
"details": {
"capital": "Sacramento",
"largestCity": "Los Angeles",
"area": "163,696 sq mi",
"statehood": "September 9, 1850",
"gdp": "$3.4 trillion",
"industries": ["Technology", "Entertainment", "Agriculture", "Tourism"]
}
},
{
"id": 2,
"name": "Texas",
"population": "29.1M",
"region": "South",
"details": {
"capital": "Austin",
"largestCity": "Houston",
"area": "268,596 sq mi",
"statehood": "December 29, 1845",
"gdp": "$1.9 trillion",
"industries": ["Energy", "Technology", "Agriculture", "Manufacturing"]
}
},
{
"id": 3,
"name": "Florida",
"population": "21.5M",
"region": "South",
"details": {
"capital": "Tallahassee",
"largestCity": "Jacksonville",
"area": "65,758 sq mi",
"statehood": "March 3, 1845",
"gdp": "$1.1 trillion",
"industries": ["Tourism", "Agriculture", "International Trade", "Aerospace"]
}
},
{
"id": 4,
"name": "New York",
"population": "19.5M",
"region": "Northeast",
"details": {
"capital": "Albany",
"largestCity": "New York City",
"area": "54,555 sq mi",
"statehood": "July 26, 1788",
"gdp": "$1.7 trillion",
"industries": ["Finance", "Media", "Technology", "Healthcare"]
}
},
{
"id": 5,
"name": "Illinois",
"population": "12.7M",
"region": "Midwest",
"details": {
"capital": "Springfield",
"largestCity": "Chicago",
"area": "57,914 sq mi",
"statehood": "December 3, 1818",
"gdp": "$875 billion",
"industries": ["Manufacturing", "Finance", "Agriculture", "Transportation"]
}
}
]
}
Check out Handle Dropdown Change Event in jQuery
Step 3: JavaScript to Load and Display Data
document.addEventListener('DOMContentLoaded', function() {
// Fetch the data
fetch('states.json')
.then(response => response.json())
.then(data => {
const tableBody = document.getElementById('tableBody');
// Create rows for each state
data.states.forEach(state => {
// Create parent row
const parentRow = document.createElement('tr');
parentRow.className = 'parent-row';
parentRow.setAttribute('data-id', state.id);
parentRow.innerHTML = `
<td><span class="expand-btn" data-id="${state.id}">+</span></td>
<td>${state.name}</td>
<td>${state.population}</td>
<td>${state.region}</td>
`;
tableBody.appendChild(parentRow);
// Create hidden child row (it will be populated when expanded)
const childRow = document.createElement('tr');
childRow.className = 'child-row hidden';
childRow.setAttribute('data-parent', state.id);
childRow.innerHTML = `<td colspan="4"><div class="child-content" id="content-${state.id}">Loading...</div></td>`;
tableBody.appendChild(childRow);
});
// Add event listeners to expand buttons
const expandButtons = document.querySelectorAll('.expand-btn');
expandButtons.forEach(btn => {
btn.addEventListener('click', function() {
const stateId = this.getAttribute('data-id');
const parentRow = this.closest('tr');
const childRow = document.querySelector(`.child-row[data-parent="${stateId}"]`);
const childContent = document.getElementById(`content-${stateId}`);
// Toggle button appearance
this.textContent = this.textContent === '+' ? '-' : '+';
this.classList.toggle('active');
// Toggle child row visibility
childRow.classList.toggle('hidden');
// Load content if it's the first expansion
if (childContent.textContent === 'Loading...') {
// Find the state data
const stateData = data.states.find(state => state.id == stateId);
const details = stateData.details;
// Create the content HTML
let contentHTML = `
<div class="details-grid">
<div class="detail-item">
<strong>Capital:</strong> ${details.capital}
</div>
<div class="detail-item">
<strong>Largest City:</strong> ${details.largestCity}
</div>
<div class="detail-item">
<strong>Area:</strong> ${details.area}
</div>
<div class="detail-item">
<strong>Statehood:</strong> ${details.statehood}
</div>
<div class="detail-item">
<strong>GDP:</strong> ${details.gdp}
</div>
<div class="detail-item">
<strong>Major Industries:</strong> ${details.industries.join(', ')}
</div>
</div>
`;
childContent.innerHTML = contentHTML;
}
});
});
})
.catch(error => {
console.error('Error loading data:', error);
document.getElementById('tableBody').innerHTML = '<tr><td colspan="4">Error loading data. Please try again later.</td></tr>';
});
});
Read Execute Functions After Page Load Using jQuery
Step 4: Add Additional CSS for the Dynamic Version
/* Add these styles to your existing CSS */
.details-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-gap: 15px;
padding: 10px;
}
.detail-item {
background-color: white;
padding: 10px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}