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;
}
This method improves the performance by loading detailed data only when needed, which is particularly useful for large datasets.
Check out Check Which Radio Button is Selected Using jQuery
Method 3 – Use jQuery for Simplicity
If you’re already using jQuery in your project, here’s an even simpler implementation:
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>jQuery Expandable Table</title>
<link rel="stylesheet" href="styles.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="container">
<h1>U.S. Company Financial Data</h1>
<table id="expandableTable">
<thead>
<tr>
<th></th>
<th>Company</th>
<th>Industry</th>
<th>Revenue (2024)</th>
</tr>
</thead>
<tbody>
<tr class="parent-row">
<td><span class="expand-btn">+</span></td>
<td>Apple Inc.</td>
<td>Technology</td>
<td>$394.3B</td>
</tr>
<tr class="child-row">
<td colspan="4">
<div class="child-content">
<h3>Company Details</h3>
<p><strong>Headquarters:</strong> Cupertino, California</p>
<p><strong>CEO:</strong> Tim Cook</p>
<p><strong>Founded:</strong> April 1, 1976</p>
<p><strong>Employees:</strong> 164,000+</p>
<p><strong>Products:</strong> iPhone, Mac, iPad, Apple Watch, Apple TV, AirPods</p>
</div>
</td>
</tr>
<tr class="parent-row">
<td><span class="expand-btn">+</span></td>
<td>Microsoft Corporation</td>
<td>Technology</td>
<td>$211.9B</td>
</tr>
<tr class="child-row">
<td colspan="4">
<div class="child-content">
<h3>Company Details</h3>
<p><strong>Headquarters:</strong> Redmond, Washington</p>
<p><strong>CEO:</strong> Satya Nadella</p>
<p><strong>Founded:</strong> April 4, 1975</p>
<p><strong>Employees:</strong> 181,000+</p>
<p><strong>Products:</strong> Windows, Office, Azure, Xbox, Surface</p>
</div>
</td>
</tr>
<tr class="parent-row">
<td><span class="expand-btn">+</span></td>
<td>Amazon.com Inc.</td>
<td>E-commerce, Cloud</td>
<td>$513.9B</td>
</tr>
<tr class="child-row">
<td colspan="4">
<div class="child-content">
<h3>Company Details</h3>
<p><strong>Headquarters:</strong> Seattle, Washington</p>
<p><strong>CEO:</strong> Andy Jassy</p>
<p><strong>Founded:</strong> July 5, 1994</p>
<p><strong>Employees:</strong> 1,540,000+</p>
<p><strong>Products/Services:</strong> E-commerce, AWS, Prime Video, Alexa, Kindle</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<script src="jquery-script.js"></script>
</body>
</html>
Step 2: jQuery Script
$(document).ready(function() {
// Hide all child rows initially
$('.child-row').hide();
// Click handler for the expand buttons
$('.expand-btn').click(function() {
// Toggle the button text
$(this).text($(this).text() === '+' ? '-' : '+');
$(this).toggleClass('active');
// Find and toggle the next child row
$(this).closest('tr').next('.child-row').toggle('fast');
});
});
This jQuery version is much more concise while still providing the same functionality.
Expandable tables represent just one technique in the broader toolkit of interactive data visualization. The best approach depends on your specific use case, audience needs, and technical constraints.
By mastering these techniques, you’ll be able to present large, complex datasets in a way that’s both manageable and intuitive, allowing users to find exactly what they need without drowning in information they don’t
I hope this guide has given you the tools you need to create more user-friendly data presentations in your web applications. Happy coding!
Other tutorials you may read:

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.