Pivot Table — Cross-Tab HTML CSS JS (No Library)

Pivot Table · Tables · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Cross-tabulation
Reshapes flat records into a row × column summary matrix.
Switchable measures
Sum, average, or count — all through one aggregate function for consistency.
Row and column totals
A Total column and Total row summarise each axis.
Correct non-additive totals
Totals aggregate the raw records, so averages stay correct (not an average of averages).
Grand total
The corner of the totals aggregates the whole dataset.
Aligned numeric grid
Right-aligned tabular cells and left-aligned headers keep columns readable.
Horizontal scroll
The table scrolls when there are many columns.
Data-driven & no library
Pivots a flat DATA array by configurable fields in plain HTML/CSS/JS.

About this UI Snippet

Pivot Table — Cross-Tabulate Flat Records by Row and Column with Totals

Screenshot of the Pivot Table snippet rendered live

A pivot table turns a flat list of records into a cross-tabulation: pick a field for the rows, a field for the columns, and a measure for the cells, and it summarises the data into a grid with totals — the core of every spreadsheet pivot and BI tool. This snippet builds a working pivot table in plain HTML, CSS, and vanilla JavaScript, aggregating the data itself with switchable sum/average/count measures and full totals — no library.

Cross-tabulation from flat data

The input is a flat array of records (region, quarter, sales). The pivot derives the unique row values and unique column values, then for each row/column intersection filters the records that match both and aggregates them into a cell. This filter-and-aggregate at every intersection is the essence of a pivot — it reshapes one-row-per-event data into a summary matrix, which is exactly what makes raw transactional data readable.

Switchable measures

A selector changes the aggregation across the whole table — sum, average, or count — and re-pivots live. aggregate(records, how) is the single function that defines each measure: count is the record tally, sum totals the value field, average divides the two. Because every cell, row total, column total, and the grand total all route through this one function, switching the measure recomputes the entire grid consistently — there's no chance of the totals using a different calculation than the cells.

Totals on both axes

The table adds a Total column (each row aggregated across all columns), a Total row in the footer (each column aggregated across all rows), and a grand total at their intersection. Crucially, totals are computed from the *raw records*, not by summing the displayed cells — this matters for the average measure, where averaging the cell averages would be wrong; aggregating the underlying records gives the correct overall average. Getting totals right under non-additive measures is the detail that separates a real pivot from a naive grid.

Readable, scrollable layout

Row headers and the corner are left-aligned while numeric cells are right-aligned with tabular figures so columns line up, totals are emphasised with a tinted background, and the table scrolls horizontally if there are many columns. These are the conventions that make a dense numeric grid scannable.

Data-driven and drop-in

Change the ROW, COL, and VAL field names at the top to pivot any flat dataset by different dimensions, or wire those to selectors for a fully interactive pivot. It's a clear, dependency-free reference for the cross-tabulation and aggregation logic behind every pivot table. The filter-per-cell approach here is intentionally simple and easy to follow for a dataset this size; it scans the full DATA array once per intersection, so it's O(rows × cols × records) — fine for dozens of cells, but for a pivot over thousands of records you'd first group records once into a Map keyed by row+col, then read each cell's aggregate from that map in O(1). The same regrouping approach extends naturally to a pivot with more than two dimensions — a third "page" field, say — by keying the map on all three values and adding a selector that filters which page's slice is currently rendered, without changing how the row/column grid itself is built.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to work through the aggregation edge cases by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the footer and row totals must aggregate the raw filtered records rather than summing the already-computed cell values, and specifically why that distinction matters for the average measure but not for sum or count. The same assistant can help optimize it, for example checking whether the current approach of filtering the full DATA array once per row/column intersection becomes a real bottleneck as the dataset grows, and how to replace it with a single grouping pass into a lookup map keyed by row and column. It's also useful for extending the effect: ask it to add a third pivot dimension (a "page" filter that slices which subset of data is shown), support sorting rows or columns by their total, or add CSV export of the rendered grid. 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 pivot table that cross-tabulates a flat array of records in plain HTML, CSS, and vanilla JavaScript, with no charting or spreadsheet library.

Requirements:
- Start from a flat array of plain objects (for example, each with a region, a quarter, and a numeric sales value) and three configurable field names: which field becomes the row dimension, which becomes the column dimension, and which numeric field is being measured.
- Derive the unique values for the row dimension and the unique values for the column dimension directly from the data (not hardcoded), and build a table where every row/column intersection is a cell showing the aggregated measure for exactly the records matching both that row's value and that column's value.
- Implement a single aggregate function that supports at least three modes — sum, average, and count — and route every cell, every row total, every column total, and the grand total through that one function so switching modes can never leave some totals using a different calculation than others.
- Add a dropdown that lets the user switch the aggregation mode live, causing the entire grid (cells and all totals) to recompute and re-render immediately.
- Add a totals column at the right of each row and a totals row at the bottom of the table, plus a grand total in their intersection — and make sure every one of these totals is computed by aggregating the underlying raw records that match that slice, not by summing the already-rendered cell values, so the average mode's totals stay mathematically correct.
- Style numeric cells with right-aligned, tabular-figure text and header/label cells left-aligned, and make the table scroll horizontally if there are more columns than fit the container.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA pivot table renders, cross-tabulating sales by region (rows) and quarter (columns).
  2. 2
    Switch the measureUse the Measure selector to re-pivot as sum, average, or count.
  3. 3
    Read the totalsA Total column, Total row, and grand total summarise both axes.
  4. 4
    Change the dimensionsEdit the ROW, COL, and VAL field names to pivot by different fields.
  5. 5
    Swap in your dataReplace the flat DATA array with your own records.
  6. 6
    Make it interactiveWire ROW/COL/VAL to selectors to let users choose the pivot dimensions.

Real-world uses

Common Use Cases

Sales and revenue reports
Cross-tab sales by region and period — pair with a bar chart of the totals.
Analytics breakdowns
Summarise events by two dimensions alongside a data table of the raw rows.
Finance and budgeting
Pivot spend by category and month with correct subtotals.
Survey cross-tabs
Cross-tabulate responses by two questions.
Inventory by location and type
Summarise stock across two dimensions.
Learning cross-tabulation
A reference for pivot aggregation and totals — compare with a grouped rows table.

Got questions?

Frequently Asked Questions

A normal table shows one row per record. A pivot table cross-tabulates: it picks a field for the rows and a field for the columns, then summarises a measure at each intersection. This reshapes flat, one-row-per-event data into a compact summary grid — turning, say, twelve sales records into a region-by-quarter matrix with totals.

For additive measures like sum or count, summing the cells happens to give the right total. But for average, averaging the displayed cell-averages is mathematically wrong — it ignores how many records each cell represents. Aggregating the underlying raw records for every total guarantees correctness across all measures, which is essential for a trustworthy pivot.

Change the ROW, COL, and VAL constants to the field names you want as rows, columns, and the measured value. The code derives the unique values and aggregates generically, so any flat dataset pivots by any of its fields. To make it interactive, bind those constants to dropdowns and call render() on change.

Yes — the grid is generated from the unique values in your data, so it scales to any number of rows and columns, and the container scrolls horizontally when columns overflow. For very large datasets you'd precompute the aggregation once (e.g. group records into a map keyed by row+column) instead of filtering per cell, but the logic is the same.

In React, hold the data and measure in state and compute the pivoted structure with useMemo, then render the grid from it; in Vue, use a computed pivot object with v-for; in Angular, a getter with *ngFor. The uniq()/aggregate() cross-tab logic is framework-agnostic — only the measure state and rendering move into the framework.