Expandable Table — Free HTML CSS JS Snippet

Expandable Rows Table · Tables · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Expandable rows: display:none→table-row toggle via .open class on detail tr
Accordion: closes all open rows before opening new one (single-open mode)
Chevron rotation: transform:rotate(90deg) with 0.2s transition on .open
Inner detail table: colspan spans all columns, left border accent, indented
Status badges: .badge.green/.yellow/.blue/.red with rgba tinted backgrounds
Customer column: gradient avatar initials + name in flex row
Order ID: monospace font + indigo colour for visual scan differentiation
Nested data hierarchy: parent summary row + child line-item detail table

About this UI Snippet

Expandable Rows Table — Parent Rows with Nested Detail Tables, Chevron Toggle & Accordion Behaviour

Screenshot of the Expandable Rows Table snippet rendered live

An expandable rows table shows summary data in each row and reveals detailed sub-data when the row is clicked — without navigating to a new page. This pattern is used in order management tables (click an order to see line items), expense reports (click a category to see individual transactions), file managers (click a folder to see files), and any data set with a parent-child hierarchy. This snippet provides a complete implementation with chevron rotation, accordion single-open behaviour, an inner detail table, status badges, and customer avatar initials.

How the expand/collapse works

Each main row has a data-id attribute. The detail row below it has id="detail-N" matching the data-id. When a row is clicked, toggle(row) reads the data-id, finds the detail row, and adds/removes the .open class on both. The detail row defaults to display: none via CSS and switches to display: table-row when .open is applied.

Accordion behaviour

Before opening a new row, toggle() closes all currently open rows: document.querySelectorAll(".main-row.open").forEach(r => { r.classList.remove("open"); detail.classList.remove("open"); }). This ensures only one row is expanded at a time — the accordion pattern — which keeps the table scannable. Remove the "close all others" block to allow multiple rows to be open simultaneously.

The chevron indicator

A › character in the first column rotates 90 degrees via transform: rotate(90deg) when .open is applied to the parent row. The CSS transition: transform 0.2s animates the rotation smoothly. The colour changes from grey to indigo to signal the active state.

The inner detail table

The detail row contains a full colspan="7" cell (spanning all columns). Inside it, a .detail-wrap div with a left border accent (border-left: 3px solid #6366f1) and indentation creates visual hierarchy. Inside .detail-wrap, a nested .inner-table shows line-item data (product name, SKU, quantity, price, subtotal). The inner table uses the same border-collapse collapse style as the outer table for visual consistency.

Customising the table

Update the outer table columns (Order ID, Customer, Date, Items, Total, Status) to match your data model. Change the inner table columns to match your detail data. Update the colspan value on the detail td to match your column count. Add or remove badge colour classes as needed. For a flat list instead, use the Data Table snippet; for true hierarchical nesting, see the tree table.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to trace the row-to-detail wiring by eye. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the data-id attribute on each main row is used to find and toggle its matching detail row by constructed id, and why the toggle function closes every other open row before opening the clicked one. The same assistant can help optimize it — ask whether hardcoding every order's line items directly in the HTML will scale once there are hundreds of orders, and what a lazy-fetch-on-first-expand approach would need to change in the toggle function. It's also useful for extending the table: ask it to add a smooth height animation instead of the instant display toggle, support expanding multiple rows at once as an option, or add a small loading spinner inside the detail row while its data is being fetched for the first time. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a table with expandable master-detail rows in plain HTML, CSS, and JavaScript — no library.

Requirements:
- Each summary row must carry a data-id attribute, and immediately following it in the same tbody, a matching detail row whose id is derived from that same identifier (for example "detail-" plus the id), so the two rows can be found and linked programmatically without any external mapping.
- The detail row must be a single td with a colspan equal to the total number of columns in the outer table, containing a nested, completely independent inner table with its own columns and widths for the line-item detail.
- The detail row must be hidden by default via CSS (display: none) and switched to display: table-row only when an "open" class is applied — never removed from the DOM, so the same markup can be toggled back and forth cheaply.
- Clicking a summary row must first find and close every other row that is currently open (removing the open class from both its summary and detail row), then toggle open state on the clicked row and its detail row — enforcing that only one row's detail is visible at a time.
- A chevron indicator in the first column of each summary row must rotate 90 degrees via a CSS transform transition when its row is open, and change color to signal the active state.
- Give the detail panel a visually distinct left border accent and indentation relative to the outer table so it reads clearly as a nested, subordinate data view rather than another top-level row.
- Style at least three status badge variants with tinted backgrounds for the summary row's status column.

Step by step

How to Use

  1. 1
    Click any row to expand its detailClicking a row reveals the nested detail table with line items. The chevron rotates 90 degrees and the row gets a background tint. Clicking another row closes the open one (accordion behaviour).
  2. 2
    Update the outer table dataIn the HTML, edit each main-row tr: change order ID, customer initials and gradient, date, item count, total amount, and badge class/text. Update the data-id attribute and matching detail row id accordingly.
  3. 3
    Update the inner detail tableInside each detail-row, edit the inner-table tbody rows with your actual line-item data. Adjust the number of columns in the inner thead and tbody to match your data structure.
  4. 4
    Allow multiple rows open simultaneouslyRemove the "Close all others" block in the toggle() function: delete the document.querySelectorAll(".main-row.open").forEach(...) lines. The function will then only toggle the clicked row without affecting others.
  5. 5
    Add more columns or rowsAdd th headers to the outer table thead and matching td cells to each main-row tr. Update the colspan="7" on each detail-row td to match the new total column count. Duplicate the main-row + detail-row pair for each new data record.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component managing open state in useState, or "Tailwind" for a React + Tailwind CSS version.

Real-world uses

Common Use Cases

Order management tables with line-item detail
The classic use case — an order list where each row expands to show the individual products, SKUs, quantities, and prices. Trigger a detail fetch on first expand to avoid loading all line items upfront: fetch("/api/orders/"+id+"/items") on first open, then cache the result.
Expense reports and financial transaction tables
Expense category rows (Travel, Software, Marketing) expand to show individual transactions within each category. The accordion behaviour keeps the totals row visible while reviewing line items, making it easy to compare categories.
File manager and directory tree tables
Folder rows expand to show file rows. The chevron pattern is identical to a file system tree navigator. Nest further by making inner rows also expandable — set up recursive expand logic with depth-based indentation on the detail wrapper.
Project and milestone tracking tables
Project rows expand to show task lists. Each task can show assignee, due date, status, and time logged. The left border accent on the detail panel creates visual grouping — use different border colours per project status.
Study the master-detail table pattern and colspan technique
The expandable row technique — a summary tr followed by a detail tr in the same tbody — is a pure HTML table pattern that requires no overlay, no drawer, and no separate panel. It shares the single-open accordion logic of the accordion FAQ. The colspan="N" cell spanning all columns inside the detail row is the key technique that makes the inner content span the full table width.
Employee hierarchy and department tables
Department rows (Engineering, Design, Marketing) expand to show individual employee rows with role, salary band, and start date. The accordion single-open behaviour is especially useful for HR tables where only one department needs to be visible at a time.

Got questions?

Frequently Asked Questions

The detail row has a single td element with colspan="7" — or whatever number matches the total column count in the outer table. This single cell spans the full row width. Inside it, a div with padding and a left border creates the indented detail panel. The inner-table inside this div is a completely separate table, not constrained by the outer column widths, so it can have different columns and widths.

Track which rows have been loaded: const loaded = new Set(). In toggle(), before opening a new row, check if loaded.has(id). If not: show a loading spinner in the detail row, fetch("/api/orders/"+id+"/items").then(data => { renderDetail(id, data); loaded.add(id); row.classList.add("open"); detail.classList.add("open"); }). The detail row content is rendered from the API response on first expand and cached for subsequent expands.

CSS cannot animate from height: 0 to height: auto directly. Instead: set the detail row to display: table-row always (remove display:none), and use max-height animation: max-height: 0; overflow: hidden on the detail div, transitioning to max-height: 500px on .open. Or use the Web Animations API: detail.animate([{height: "0"},{height: detail.scrollHeight+"px"}], {duration: 250, easing: "ease"}).

Click "JSX" to download. Manage open row state with useState<string|null>(null) for accordion mode (one open at a time) or useState<Set<string>>(new Set()) for multi-open mode. Render the detail tr conditionally: {openId === row.id && <tr><td colSpan={7}><DetailContent data={row.items} /></td></tr>}. For lazy data loading, use useState to track per-row data and fetch on first expand inside a click handler.