Source Code

<div class="wrap">
  <div class="table-head">
    <h2 class="table-title">Expense Line Items</h2>
    <span class="hint">Double-click a Qty or Unit cost cell to edit</span>
  </div>
  <div class="table-scroll">
    <table class="tbl">
      <thead>
        <tr>
          <th>Item</th>
          <th>Category</th>
          <th class="num">Qty</th>
          <th class="num">Unit cost</th>
          <th class="num">Line total</th>
        </tr>
      </thead>
      <tbody id="tbody"></tbody>
      <tfoot>
        <tr>
          <td colspan="2">Totals</td>
          <td class="num" id="totalQty">0</td>
          <td class="num">&mdash;</td>
          <td class="num" id="totalCost">$0.00</td>
        </tr>
      </tfoot>
    </table>
  </div>
</div>

Table with Sticky Footer Totals Row — Free HTML CSS JS Snippet

Table with Sticky Footer Totals Row · Tables · Plain HTML, CSS & JS · Live preview

What's included

Features

Independent sticky header and sticky footer, both scoped to the scrollable table container via pure CSS
Footer totals always recomputed fresh from the full dataset, never incrementally accumulated
Footer row briefly flashes on every recalculation to draw attention to an off-screen-context change
Editing requires a double-click rather than single-click to avoid accidental edits while scanning
Line totals are always derived (qty times unit cost), never independently editable or able to drift out of sync
Commit-on-blur ensures clicking away from an in-progress edit still saves the typed value
Currency values formatted consistently with toLocaleString for two decimal places
Works with any number of rows — totals and rendering logic iterate the data array directly

About this UI Snippet

Table with Sticky Footer Totals Row — Sticky Header/Footer & Live Recomputed Totals

Screenshot of the Table with Sticky Footer Totals Row snippet rendered live

A long expense or line-item table loses its most useful number the moment the totals row scrolls out of view — a reader has to scroll all the way down just to see what everything adds up to, then scroll back up to keep reviewing individual rows. This snippet keeps both ends pinned: a sticky header stays visible while scrolling down through rows, and a sticky footer totals row stays visible at the bottom of the scroll container the entire time, recomputing live whenever an underlying value is edited.

Two independent sticky elements inside one scroll container

Both .tbl thead and .tbl tfoot use position: sticky — the header with top: 0 pinning it to the top of .table-scroll's scrollable area, and the footer with bottom: 0 pinning it to the bottom. Because the sticky positioning is scoped to .table-scroll (which has overflow-y: auto and a fixed max-height) rather than the page itself, both bars stay fixed relative to the table's own scrolling region without needing any JavaScript scroll listeners or manual repositioning — pure CSS handles both independently.

Totals recomputed from source data, never accumulated incrementally

updateTotals() always recalculates totalQty and totalCost fresh with .reduce() over the full ITEMS array rather than tracking a running total that gets incremented or decremented as individual line items change. This avoids an entire class of bugs where an incremental total silently drifts from the true sum after several edits — recomputing from source on every render guarantees the footer is always exactly correct, at the cost of a full array pass each time, which is negligible for any table size a human would actually scroll through.

A brief flash confirms the totals actually changed

Every call to updateTotals() adds a .flash class to the footer row (darkening its background briefly) and removes it 300ms later. Because the footer is the one row that's always visible regardless of scroll position, it's also the row most likely to change without the user's eyes already being on it — someone editing a quantity cell near the top of a long table is looking at that cell, not the footer 20 rows below. The flash draws their attention to the fact that the total, which they may not even be looking at, just updated in response to their edit.

Editing is double-click, not single-click, to avoid accidental edits

Only qty and unitCost cells carry the .editable class, and entering edit mode requires a dblclick rather than a single click — a deliberate choice for a table where most interaction is scrolling and reading, not editing, so a single stray click while scanning the table doesn't accidentally pop open an edit field. The line-total column is never editable directly since it's always derived as qty * unitCost.

Commit-on-blur mirrors the same safety net used in the keyboard-navigable table

Like other editable-cell patterns in this library, the temporary input commits its value on Enter, discards on Escape (by simply re-rendering from unmodified ITEMS), and also commits on blur — so clicking elsewhere on the page while mid-edit still saves rather than silently losing the typed value.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the sticky header and sticky footer both work independently with pure CSS and no scroll event listeners, and what CSS property on the wrapping container makes that possible. The same assistant can help optimize it — for instance asking whether recomputing totals with a full array reduce on every edit would still be performant for a table with several thousand rows, or whether a running-total approach would become worth the added complexity at that scale. It's also useful for extending the table: ask it to add a per-category subtotal row, support multi-currency line items, or add a CSV export button that includes the footer totals row. 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 scrollable data table with a sticky header and a sticky footer totals row in plain HTML, CSS, and JavaScript — no framework, no library.

Requirements:
- Render a table of line items (name, category, quantity, unit cost, and a computed line total equal to quantity times unit cost) inside a container with a fixed maximum height and vertical scrolling.
- The table header row must remain pinned to the top of the scrollable area while the body scrolls beneath it, and a separate footer row showing column totals must remain independently pinned to the bottom of the same scrollable area — both using pure CSS sticky positioning, with no JavaScript scroll listeners or manual repositioning.
- The footer's total quantity and total cost must always be recalculated by summing the full underlying dataset from scratch every time it updates, not maintained as an incrementally adjusted running total.
- Make the quantity and unit cost cells editable via double-click (not single click), replacing the cell's content with a text input pre-filled with the current value; pressing Enter or clicking away from the input must save the new value and immediately recompute that row's line total and the footer's totals, while pressing Escape must discard the edit and restore the original value.
- Whenever the footer totals recompute, the footer row must briefly flash a different background color and then fade back to its normal appearance, to draw attention to a total that changed off-screen from wherever the user was actually editing.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Scroll the tableThe header row stays pinned to the top and the totals row stays pinned to the bottom of the scroll area the entire time.
  2. 2
    Double-click a Qty or Unit cost cellOpens an inline input pre-filled with the current value, selected for immediate typing.
  3. 3
    Press Enter or click away to saveThe line total and the sticky footer totals recompute immediately, and the footer briefly flashes to confirm the change.
  4. 4
    Press Escape to cancel an editReverts to the unmodified value without saving.
  5. 5
    Replace ITEMS with real dataUpdate the array with your own line items — totals and line-item math recompute automatically from qty and unitCost.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a Tailwind CSS version.

Real-world uses

Common Use Cases

Expense reports and reimbursement tools
The core use case — let someone review and adjust individual line items while always seeing the running total without losing their scroll position.
Invoice and quote line-item editors
Pair with the Formula Calculated Columns pattern for a spreadsheet-like invoice builder with live totals.
Budget and cost-tracking dashboards
Any table where the sum of a column matters as much as the individual rows benefits from keeping that sum permanently visible.
Order and cart summary tables
Shopping cart or order-review tables with many line items can keep a live subtotal pinned at the bottom during a long scroll.
Learn independent sticky header/footer CSS
A clean example of pinning both ends of a scrollable table with pure CSS position: sticky, no JavaScript scroll handling required.

Got questions?

Frequently Asked Questions

Both thead and tfoot use CSS position: sticky — the header with top: 0 and the footer with bottom: 0 — scoped inside .table-scroll, which has overflow-y: auto and a fixed max-height. Because sticky positioning is relative to the nearest scrolling ancestor, both bars pin to their respective edges of that specific scroll container with no JavaScript needed.

updateTotals() runs a fresh .reduce() over the entire ITEMS array every time it is called, rather than adding or subtracting a delta from a previously stored total. This guarantees the footer is always exactly correct even after many edits, avoiding the class of bugs where an incrementally maintained running total slowly drifts from the true sum due to a missed update somewhere.

The footer stays visible regardless of scroll position, but a user editing a cell higher up in a long table is not necessarily looking at the footer when it changes. The brief .flash class (a temporary background darken-and-revert) draws their attention to the fact that the total just updated in response to their edit, even though their eyes were elsewhere.

Most interaction with this kind of table is scrolling and reading rather than editing, so requiring a deliberate double-click to enter edit mode avoids accidentally opening an edit field from an ordinary single click while scanning down the rows.

No — line totals are always computed as qty times unitCost inside render() and are never given the .editable class. Editing either the quantity or unit cost automatically recalculates the correct line total and the footer totals on the next render, avoiding a state where a manually edited line total could disagree with its own quantity and unit cost.

The keydown handler calls render() directly without applying the typed value, which redraws the cell from the unmodified ITEMS array — effectively discarding whatever was typed and restoring the original value with no separate "revert" logic needed.