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 identifier
  • data-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.