webdevpuneet.com

Sticky Summary Row — Pinned Totals Footer HTML CSS JS

Sticky Summary/Totals Row · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="tsr-card">
  <div class="tsr-head">
    <h3>Monthly expenses</h3>
    <p class="tsr-hint">Scroll the table — totals stay pinned to the bottom.</p>
  </div>
  <div class="tsr-scroll" id="tsrScroll">
    <table class="tsr-table">
      <thead><tr><th>Category</th><th class="tsr-num">Budget</th><th class="tsr-num">Spent</th><th class="tsr-num">Remaining</th></tr></thead>
      <tbody id="tsrBody"></tbody>
      <tfoot><tr id="tsrSummary"></tr></tfoot>
    </table>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), 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

Real position:sticky footer
The summary row pins to bottom: 0 within the scroll container, the vertical counterpart to a sticky header.
Live-computed totals via reduce
Sums are calculated from the actual row array on every run, never hardcoded.
Derived aggregate fields
Remaining total is computed from summed budget minus summed spent, matching per-row logic.
Consistent conditional styling
The same over-budget check applies identically to individual rows and the aggregate total.
Elevated z-index and opaque background
Scrolling rows never visually bleed through the pinned summary row.
Sticky header and footer together
Column labels stay visible at the top while totals stay visible at the bottom, simultaneously.
Visually distinct summary row
A heavier weight and accent top border set the total apart from ordinary data rows.
Data-driven, not markup-driven
Adding, removing, or editing rows requires no manual update to the totals anywhere.

About this UI Snippet

Sticky Summary Row — Pinned Totals Computed Live From Real Row Data

Screenshot of the Sticky Summary/Totals Row snippet rendered live

A totals row buried at the bottom of a long scrollable table is only useful the moment you happen to scroll all the way down to it — the rest of the time, the sums you actually want to reference while scanning the data are out of view. This snippet keeps the summary row pinned to the bottom of the visible scroll area at all times using genuine position: sticky, with every total computed live from the row data rather than typed in by hand.

A tfoot pinned with position: sticky, not a fixed overlay

The totals row lives in a real <tfoot>, and its cells get position: sticky; bottom: 0 within the scrolling container — the browser-native mechanism for pinning an element to an edge of its nearest scrolling ancestor. This is the vertical counterpart to a sticky header table, which pins the <thead> to top: 0 the same way; here it's the footer pinned to the bottom instead, so both ends of a long table can stay visible simultaneously while only the body scrolls between them.

Real aggregation, not a hardcoded row

computeTotals() runs a genuine Array.prototype.reduce over the live CATEGORIES array, summing the budget and spent fields across every row into a single accumulator object — nothing about the totals row's numbers is typed in directly. Change a value in the source data, add or remove a category, and the totals recompute correctly the next time this function runs, because they are a direct function of the actual rows, never a separately maintained number that could drift out of sync.

A derived total, not just two summed totals

The remaining-budget total isn't summed directly from a "remaining" field — it's derived as totals.budget - totals.spent after the two real column sums are already computed, matching how the per-row remaining values are themselves computed as budget - spent rather than stored. This keeps a single consistent definition of "remaining" used both per-row and in aggregate, so the footer's math always agrees with what you'd get by manually summing every row's own remaining column.

Elevated z-index so scrolling content passes beneath it cleanly

As body rows scroll past underneath the pinned footer, the summary row needs a higher z-index and an opaque background or scrolling rows would visibly show through it — set here alongside a distinct top border and heavier font weight so the summary row also reads visually as the "final word" on the data above it, not just another row.

Consistent styling logic between rows and the total

The negative-remaining highlight (a warning color when a category is over budget) uses the exact same conditional check in both the per-row loop and the summary computation — remaining < 0 — so the visual language for "over budget" is identical whether you're looking at one category or the aggregate across all of them.

Customizing it

Add an average alongside the sum, compute per-column totals generically by iterating column keys instead of naming each field, or combine with a virtualized table for a sticky summary over a very large scrolling dataset.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAn eight-row expense table renders with a Total row already visible at the bottom.
  2. 2
    Scroll the table bodyThe Total row stays pinned to the bottom of the visible scroll area as rows pass beneath it.
  3. 3
    Check the numbersBudget, Spent, and Remaining totals are computed live from the actual row values via reduce.
  4. 4
    Edit a row's budget or spent valueThe totals recompute the next render — nothing about the summary is hand-typed.
  5. 5
    Add or remove a categoryThe totals update correctly to reflect exactly the rows currently in the data array.
  6. 6
    Watch an over-budget categoryIts Remaining value and the aggregate Remaining both use the same negative-value highlight.

Real-world uses

Common Use Cases

Budget and expense tracking
Keep running totals visible while scanning a long table, with the summary row pinned to bottom: 0 of the scroll container.
Financial and accounting tables
Pin sum and average rows, with a remaining total computed as summed budget minus summed spent from the real data.
Order line-item tables
Keep an order total in view while editing many lines, using reduce over the row array on every render.
Inventory and stock summaries
Pin aggregate stock counts, applying the same over-budget check to individual rows and to the summary for consistent styling.
Reporting dashboards and sticky learning
Pair with a multi-column sort table and a sticky header table for a fully pinned layout.

Got questions?

Frequently Asked Questions

As an ordinary last row, the summary would scroll out of view along with the rest of the body the moment you scroll down through a long table — exactly when a running total is most useful to have visible. position: sticky with bottom: 0 keeps it pinned to the bottom edge of the scrolling container regardless of how far the body has scrolled, the same mechanism a sticky header uses at top: 0.

Both approaches produce the same number here since subtraction distributes over the sum, but computing it from the already-summed budget and spent totals keeps a single definition of "remaining" (budget minus spent) used consistently everywhere, rather than maintaining what amounts to the same calculation in two separate places that could theoretically diverge if the formula ever changed in only one spot.

Inside computeTotals, alongside the running budget and spent sums, either divide the final sums by rows.length after the reduce completes, or accumulate a count and divide inside the reduce callback — then render an additional summary value using that computed average the same way the sums are rendered.

Instead of naming budget and spent explicitly in computeTotals, pass in an array of column keys to sum and use reduce with a loop over those keys (acc[key] = (acc[key] || 0) + row[key]) inside the reducer — this way adding a new numeric column to the row data only requires adding its key to the columns array, not editing the aggregation function itself.

Compute the totals with the same reduce logic inside a memoized calculation (useMemo, a computed property, or a getter) that re-runs when the underlying row data changes, and render the tfoot's sticky-positioned cells from that computed value — the CSS position:sticky behavior needs no JavaScript and ports unchanged into any framework's table markup.