Grouped Rows Table — Collapsible Group HTML CSS JS

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

Share & Support

What's included

Features

View-layer grouping
Flat data with a group field is folded into sections at render — no pre-nested data needed.
Per-group subtotals & counts
Each header shows its item count and a reduce-computed subtotal.
Collapsible groups
Clicking a header toggles its rows with a rotating caret, via one delegated listener.
Expand / collapse all
A toolbar button folds or unfolds every group together.
Grand total footer
The footer sums every row regardless of which groups are collapsed.
Hierarchy alignment
Indented items and right-aligned tabular amounts read as a financial report.
Robust row matching
Items link to their header by a shared data-group index, stable as groups change.
Data-driven & no library
Renders from an ITEMS array keyed by its group field — zero dependencies.

About this UI Snippet

Grouped Rows Table — Collapsible Category Groups with Subtotals & Grand Total

Screenshot of the Grouped Rows Table snippet rendered live

When a table's rows fall into natural categories — expenses by type, tasks by project, sales by region — grouping them with collapsible headers and subtotals turns a long flat list into a scannable, summarisable report. This snippet builds that grouped table in plain HTML, CSS, and vanilla JavaScript: rows clustered under category headers, each header showing a count and subtotal, individually collapsible with a caret, plus expand/collapse-all and a grand total — no library.

Grouping flat data into sections

The data is a flat ITEMS array where each row carries a group field. groupBy() folds that into an ordered map of group → rows, and the renderer emits a header row for each group followed by its item rows. Keeping the source flat (rather than pre-nested) is deliberate: it's the shape data actually arrives in from an API or database, and grouping in the view means you can regroup by a different field just by changing the key — without restructuring your data.

Headers that summarise

Each group header shows the category name, the number of items in it, and the group's subtotal (a reduce over its rows). Surfacing the subtotal on the header is what makes the grouping valuable — a reader can collapse every group and still see the totals per category, turning the table into a summary. A grand total in the table footer sums every row regardless of collapse state, so the bottom line is always correct.

Per-group and global collapse

Clicking any header toggles just that group: a caret rotates and the group's item rows get a hide class. The click is handled by one delegated listener on the table body via closest('.grt-group'), and rows are matched to their header by a shared data-group index — so collapsing is robust even as groups are added or reordered. A toolbar button collapses or expands *all* groups at once, flipping every caret and row together, which is the control people reach for on a table with many sections.

Indentation and alignment that read as a hierarchy

Item rows are indented under their header, amounts are right-aligned with tabular figures so they form a clean column, and the header subtotal floats to the same right edge — so subtotals and line items line up vertically into a proper financial-report layout. These alignment details are what make a grouped table read as a structured document rather than a striped list.

Data-driven and drop-in

Everything derives from the ITEMS array and its group field, so swapping in your data — or grouping by a different property — is a one-line change. Because it's dependency-free, it drops into any dashboard, expense report, or admin panel, and it's a clear reference for view-layer grouping, per-group aggregation, and delegated collapse handling.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to trace the delegated click handling by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the shared data-group index links a header row to its item rows through the closest lookup, or why groupBy folds a flat ITEMS array instead of expecting pre-nested data. The same assistant is useful for optimizing it — ask whether re-rendering the entire tbody via innerHTML on every toggle is wasteful for a table with hundreds of rows, and what a targeted class-toggle approach would look like instead. It is just as handy for extending the table: ask it to add per-group sorting, a search box that filters items while keeping group subtotals accurate, or persisted collapse state in localStorage. 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 collapsible grouped rows in plain HTML, CSS, and JavaScript — no library, no framework.

Requirements:
- Accept a flat array of row objects, each carrying a group field alongside its other fields (item name, vendor, amount).
- Write a groupBy function that folds the flat array into an ordered map of group name to its rows, preserving first-seen group order.
- Render one header row per group showing the group name, the count of items in it, and a subtotal computed with reduce over that group's rows, immediately followed by one row per item indented under it.
- Give each header row and its item rows a shared data-group index so they can be matched without relying on DOM adjacency.
- Use a single delegated click listener on the table body (not one listener per header) that finds the clicked header via closest, toggles an "open" class on it to rotate a CSS caret, and toggles a "hide" class on every item row sharing that data-group index.
- Add a toolbar button that collapses or expands every group at once by toggling the same classes on all headers and items in one pass.
- Add a table footer that always sums every row's amount into a grand total, regardless of which groups are currently collapsed.
- Right-align and use tabular-nums formatting for all monetary values so subtotals and the grand total form a clean vertical column.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAn expenses table renders with rows grouped under category headers showing counts and subtotals.
  2. 2
    Collapse a groupClick any category header to collapse or expand just that group; the caret rotates.
  3. 3
    Collapse or expand allUse the toolbar button to fold or unfold every group at once.
  4. 4
    Read the totalsEach header shows its subtotal and the footer shows the grand total across all rows.
  5. 5
    Swap in your dataReplace the ITEMS array; each row's group field determines its section.
  6. 6
    Regroup by another fieldChange groupBy to key on a different property to group by region, project, etc.

Real-world uses

Common Use Cases

Expense and budget reports
Group spend by category with subtotals — pair with a bar chart of category totals.
Tasks and issues by project
Cluster work items under project headers alongside a kanban board.
Sales by region or rep
Summarise revenue per group next to a leaderboard table.
Inventory by warehouse
Group stock with per-location counts and totals.
Invoices and line items
Group billable items by client, complementing an invoice preview.
Learning grouped data rendering
A reference for view-layer grouping and delegated collapse — compare with a tree table.

Got questions?

Frequently Asked Questions

Data usually arrives flat from an API or database — a list of rows, each tagged with a category. groupBy() folds that flat list into sections at render time, which means you can regroup by a different field just by changing the key, with no change to your underlying data. Pre-nesting would lock you into one grouping and complicate updates.

One delegated click listener on the table body catches clicks on any header via closest('.grt-group'). It toggles an open class (which rotates the caret) and adds/removes a hide class on every item row sharing that header's data-group index. Matching rows by index rather than DOM position keeps collapsing correct even as groups are added or reordered.

No — collapsing is purely visual. The grand total in the footer is computed once from every row in the data, and each group's subtotal is computed from its own rows, both independent of collapse state. So you can fold every group and still read accurate per-category subtotals and a correct bottom line.

Change groupBy to key on the property you want — e.g. it.region or it.project instead of it.group — and update the data accordingly. The header rendering, subtotals, collapse logic, and grand total all work off whatever grouping the function produces, so switching the grouping dimension is a one-line change.

In React, group the data with useMemo and render headers/items from the grouped structure, tracking open groups in state; in Vue, use a computed grouped object with v-for and a reactive open map; in Angular, use a pipe or getter with *ngFor. The grouping and aggregation logic is framework-agnostic — only the collapse state and rendering move into the framework.