Introduction
Ever needed a collapsible table that lets users expand and collapse rows dynamically? Many developers rely on jQuery plugins like TreeTable
, but what if you want a pure JavaScript solution?
In this guide, we’ll build a multi-level expandable table using only HTML, Tailwind CSS, and JavaScript. No libraries, no dependencies—just fast, lightweight code!
Why Use a JavaScript Tree Table?
✔ No jQuery needed – Runs faster, loads instantly
✔ Expandable rows – Show/hide child elements easily
✔ Multi-level hierarchy – Support for parents, children, and grandchildren
✔ Dynamic icons – Automatically switch between ▶ (collapsed) and ▼ (expanded)
✔ Fully responsive – Works on desktop and mobile
The Final Result
Here’s what we’re building:
✅ Click on a row to reveal its children
✅ Click on a child row to expand grandchildren
✅ Icons change dynamically (▶ to ▼)
✅ Siblings remain unaffected
Step 1: The HTML Structure
Let’s create the table with hierarchical rows.Each row has:
data-id="1"
– Unique identifierdata-parent="1"
– Tells the script which row it belongs under
<table id="treeTable" class="w-full ">
<thead >
<tr class="divide-gray-200 dark:divide-gray-700">
<th scope="col" class="px-6 py-3 text-xs font-medium text-gray-500 uppercase text-start dark:text-gray-500">
Name
</th>
<th scope="col" class="px-6 py-3 text-xs font-medium text-gray-500 uppercase text-start dark:text-gray-500">
Size
</th>
</tr>
</thead>
<tbody class="space-y-2">
<!-- Parent Row (Level 1) -->
<tr data-id="1" class="table-row mt-3 border-l-2 border-blue-400 parent-row hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="relative flex flex-row items-center px-6 py-4 text-sm font-medium text-gray-800 whitespace-nowrap dark:text-gray-200">
<div>Parent Item</div>
<div class="flex flex-row ml-3 text-xl cursor-pointer expand-icon" onclick="toggleRow(1)">▶</div>
</td>
<td>96KB</td>
</tr>
<!-- Child Rows (Level 2) -->
<tr data-parent="1" data-id="1-1" class="hidden border-l-2 border-blue-400 child-row child-level-1 hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="relative flex flex-row items-center px-6 py-4 ml-3 text-sm font-medium text-gray-800 whitespace-nowrap dark:text-gray-200">
<div>Child Item 1</div>
<div class="flex flex-row ml-3 text-xl cursor-pointer expand-icon" onclick="toggleRow('1-1')">▶</div>
</td>
<td>50KB</td>
</tr>
<!-- Grandchild Rows (Level 3) -->
<tr data-parent="1-1" data-id="1-1-1" class="hidden border-l-2 border-blue-400 child-row child-level-2 hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="relative flex flex-row items-center px-6 py-4 ml-6 text-sm font-medium text-gray-800 whitespace-nowrap dark:text-gray-200">
<div>Grandchild 1</div>
<div class="flex flex-row ml-6 text-xl cursor-pointer expand-icon" onclick="toggleRow('1-1-1')">▶</div>
</td>
<td>15KB</td>
</tr>
<!-- Great-Grandchild (Level 4) -->
<tr data-parent="1-1-1" class="hidden border-l-2 border-blue-400 child-row child-level-3 hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="relative flex flex-row items-center px-6 py-4 text-sm font-medium text-gray-800 ml-9 whitespace-nowrap dark:text-gray-200">
Great-Grandchild 1
</td>
<td>10KB</td>
</tr>
<!-- child item -->
<tr data-parent="1" data-id="1-2" class="hidden border-l-2 border-blue-400 child-row child-level-1 hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="relative flex flex-row items-center px-6 py-4 ml-3 text-sm font-medium text-gray-800 whitespace-nowrap dark:text-gray-200">
<div>Child Item 2</div>
<div class="flex flex-row ml-6 text-xl cursor-pointer expand-icon" onclick="toggleRow('1-2')">▶</div>
</td>
<td>30KB</td>
</tr>
<tr data-parent="1-2" data-id="1-2-1" class="hidden border-l-2 border-blue-400 child-row child-level-2 hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="relative flex flex-row items-center py-4 ml-3 text-sm font-medium text-gray-800 px-9 whitespace-nowrap dark:text-gray-200">
Grandchild 2
</td>
<td>20KB</td>
</tr>
<!-- Another Parent Row (Level 1) -->
<tr data-id="2" class="table-row mt-3 border-l-2 border-red-400 parent-row hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="relative flex flex-row items-center px-6 py-4 text-sm font-medium text-gray-800 whitespace-nowrap dark:text-gray-200">
<div>Parent Item - 2</div>
<div class="flex flex-row ml-3 text-xl cursor-pointer expand-icon" onclick="toggleRow(2)">▶</div>
</td>
<td>65KB</td>
</tr>
<!-- Child Rows (Level 2) -->
<tr data-parent="2" data-id="2-1" class="hidden border-l-2 border-red-400 child-row child-level-1 hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="relative flex flex-row items-center px-6 py-4 ml-3 text-sm font-medium text-gray-800 whitespace-nowrap dark:text-gray-200">
<div>Child Rows (Level 2) 2-1</div>
<div class="flex flex-row ml-3 text-xl cursor-pointer expand-icon" onclick="toggleRow('2-1')">▶</div>
</td>
<td>50KB</td>
</tr>
<!-- Grandchild Rows (Level 3) -->
<tr data-parent="2-1" data-id="2-1-1" class="hidden border-l-2 border-red-400 child-row child-level-2 hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="relative flex flex-row items-center px-6 py-4 ml-6 text-sm font-medium text-gray-800 whitespace-nowrap dark:text-gray-200">
<div>Grandchild Rows (Level 3) 2-1-1</div>
</td>
<td>15KB</td>
</tr>
<tr data-parent="2" data-id="2-2" class="hidden border-l-2 border-red-400 child-row child-level-1 hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="relative flex flex-row items-center px-6 py-4 ml-3 text-sm font-medium text-gray-800 whitespace-nowrap dark:text-gray-200">
<div>Child Item 2-2</div>
<div class="flex flex-row ml-3 text-xl cursor-pointer expand-icon" onclick="toggleRow('2-2')">▶</div>
</td>
<td>50KB</td>
</tr>
<tr data-parent="2-2" data-id="2-2-1" class="hidden border-l-2 border-red-400 child-row child-level-2 hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="relative flex flex-row items-center py-4 ml-3 text-sm font-medium text-gray-800 px-9 whitespace-nowrap dark:text-gray-200">
Grandchild 2-2-1
</td>
<td>20KB</td>
</tr>
</tbody>
</table>
How It Works
- Parent rows are visible by default.
- Child rows are hidden (
class="hidden"
). - Clicking the expand icon (
▶
) toggles visibility.
Step 2: The JavaScript (Expand & Collapse)
This script finds child rows and toggles their visibility when clicking an expand icon.
function toggleRow(parentId) {
const parentRow = document.querySelector(`tr[data-id="${parentId}"]`);
if (!parentRow) return; // ✅ Prevents errors if the row does not exist
console.log('parentId orig::'+parentId);
const childRows = document.querySelectorAll(`tr[data-parent="${parentId}"]`);
const icon = parentRow.querySelector(".expand-icon");
let isExpanded = false;
childRows.forEach(row => {
row.classList.toggle("hidden");
if (!row.classList.contains("hidden")) {
isExpanded = true;
}
// If a parent row is collapsed, hide all its children recursively
if (!isExpanded) {
let icon2 = row.querySelector(".expand-icon");
icon2.textContent = "▶"
collapseChildren(row.dataset.id);
}
});
// ✅ Only change the icon if it exists
if (icon) {
icon.textContent = isExpanded ? "▼" : "▶";
}
}
function collapseChildren(parentId) {
console.log('closing parent');
console.log(parentId);
const childRows = document.querySelectorAll(`tr[data-parent="${parentId}"]`);
//const childRows = document.querySelectorAll(`tr[data-id="${parentId}"]`);
childRows.forEach(row => {
row.classList.add("hidden");
const icon = row.querySelector(".expand-icon");
console.log('icon'+parentId);
console.log(icon);
if (icon) {
icon.textContent = "▶"
}
// Collapse deeper levels recursively
collapseChildren(row.dataset.id);
});
}
Step 3: Tailwind CSS for Styling
This enhances the layout by adding borders and animations.
.child-row {
transition: all 0.3s ease-in-out;
}
.expand-icon {
margin-left: 8px;
cursor: pointer;
font-size: 1.2rem;
color: #3498db;
}
.parent-row:hover,
.child-row:hover {
background-color: #f0f9ff;
}
Why This JavaScript Tree Table is Better
🔹 Pure JavaScript – No dependencies, no jQuery
🔹 Fast and lightweight – Works instantly, no extra libraries
🔹 Multi-level hierarchy – Supports unlimited depth
🔹 Smooth animations – Rows expand and collapse with a clean effect
🔹 SEO-Friendly – Uses semantic <table>
elements
This expandable table is perfect for:
- File managers
- Product categories
- Data structures
- FAQ sections
With just 40 lines of JavaScript, you get a fully functional tree table—no plugins needed!
🚀 Now it’s your turn! Try this in your project and customize it for your needs.