When it comes to presenting hierarchical data in a structured and interactive way, combining accordion layouts with data tables can be an elegant solution. Recently, I built a reusable AccordionTable component using React, TanStack Table (React Table v8), and Material UI (MUI) — and I’d like to walk you through how it works.
🎯 The Goal
- I wanted a flexible table component that:
- Displays summary rows that can be expanded to show child rows.
- Supports skeleton loading states while fetching data.
- Has consistent column widths across parent and child tables.
- Uses MUI’s clean UI for styling and accessibility. This pattern works great for reports like balance sheets, financial statements, or nested datasets where each parent row represents a summarized entity.
⚙️ Core Technologies
- React + TypeScript for component logic and type safety.
- TanStack Table v8 for data modeling and rendering flexibility.
- Material UI (MUI) for layout, styling, and accordion behavior.
🧱 Component Overview
export default function AccordionTable<T extends { id?: string; children?: any[] }>({
data,
columns,
skeleton,
}: AccordionTableProps<T>) { ... }
The component accepts:
- data: an array of objects (each can optionally include children).
- columns: TanStack column definitions.
- skeleton: optional loading configuration (show and rows). It then renders a table where each row is wrapped inside an accordion. When expanded, it displays a nested ChildTable for the row’s children.
🧩 Handling Nested Data
To handle the child rows, I created a small reusable component:
function ChildTable({ rows, columns, columnWidths }: {
rows: T[];
columns: ColumnDef<T>[];
columnWidths: string[];
}) {
const childTable = useReactTable({
data: rows,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<Table size="small">
<TableBody>
{childTable.getRowModel().rows.map((childRow) => (
<TableRow key={childRow.id}>
{childRow.getVisibleCells().map((cell, i) => (
<TableCell key={cell.id} sx={{ width: columnWidths[i] }}>
<Typography variant="bodyMdRegular" sx={{ color: "#696563" }}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Typography>
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
);
}
This nested table keeps the same column structure as the main one and aligns neatly using predefined columnWidths.
🎨 Accordion Layout with MUI
Each main row becomes an MUI Accordion, with summary and details sections:
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
{/* Summary cells */}
</AccordionSummary>
<AccordionDetails>
{/* Nested ChildTable */}
</AccordionDetails>
</Accordion>
- The summary displays the top-level values (like category totals).
- The details section renders the child rows (like subcategories). This approach keeps the UI compact and interactive while maintaining clarity.
💀 Skeleton Loading State
Before data loads, the component renders a lightweight placeholder using MUI’s :
{showSkeleton &&
Array.from({ length: skeleton.rows }).map((_, index) => (
<TableRow key={index}>
{Array.from({ length: table.getAllColumns().length }).map((_, colIndex) => (
<TableCell key={colIndex}><Skeleton /></TableCell>
))}
</TableRow>
))}
This improves perceived performance and maintains layout consistency during loading.
🧠 Key Learnings
- TanStack Table’s flexRender() makes it easy to render custom cell components.
- MUI Accordion integrates smoothly with table rows if you manage borders and padding carefully.
- Consistent column widths are crucial — define them in constants and reuse across parent and child tables.
🚀 Final Thoughts
The AccordionTable component combines the power of data tables with the clarity of hierarchical UI patterns. It’s especially useful for complex data structures like:
- Financial reports (Balance Sheet, P&L, etc.)
- Nested analytics results
- Hierarchical user or asset data This approach keeps things modular, scalable, and visually clear while maintaining accessibility and responsiveness.
Top comments (0)