webdevpuneet.com

Grouped Rows Table — Collapsible Group HTML CSS JS

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

CategoryTables

Copy & paste

Source Code

<div class="grt-wrap">
  <div class="grt-bar">
    <h3>Expenses by category</h3>
    <button type="button" class="grt-toggle" id="grtToggle">Collapse all</button>
  </div>
  <table class="grt-table">
    <thead><tr><th>Item</th><th>Vendor</th><th class="grt-num">Amount</th></tr></thead>
    <tbody id="grtBody"></tbody>
    <tfoot><tr><td colspan="2">Grand total</td><td class="grt-num" id="grtGrand"></td></tr></tfoot>
  </table>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

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.

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 per-group subtotals and counts, and show a bar chart for the same data.
Tasks by project
Cluster work items under project headers, with a link to a kanban board for the board view.
Sales by region or rep
Summarise revenue per group by region or rep, with a leaderboard table as the ranked alternative view.
Inventory by warehouse
Group stock with per-location counts, using view-layer grouping so the flat data is folded into sections only at render time.
Invoices and tree comparisons
Group billable items by client in an invoice preview style report, or compare with a tree table for deeper hierarchies.
Related: Spreadsheet Keyboard Navigation Table
See the Spreadsheet Keyboard Navigation Table for a related tables pattern worth pairing with this one.

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.