Announcing Totals and Summary Rows

Permalink to "Announcing Totals and Summary Rows"

A summary row holds an aggregate of the rows above it: a total, a subtotal, an average, a count. It is frequently the only row a user actually wants — the answer the table exists to give. And it is often marked up as one more data row with bold text, so a screen reader announces “1,700.00” under the “Amount” header with nothing to say that this is the sum rather than another invoice.

This page covers the markup that names an aggregate, where subtotals and totals belong in the DOM, and how to announce a total that changes. It belongs to row grouping & totals in data tables.

Spec reference

Permalink to "Spec reference"

<tfoot> is the table footer row group. The HTML standard allows it before or after <tbody> in source; browsers render it at the bottom either way, and the accessibility tree follows DOM order. Put it after the bodies so reading order matches visual order.

A <th scope="row"> in the summary row (“Total”) becomes the row header for every cell in that row, so the amount cell is announced as “Total, Amount, 1,700.00”. That is the whole trick: an aggregate is identified by its row header, exactly like any other row.

For changing totals, SC 4.1.3 Status Messages applies — a recalculated total after an edit or a filter is a status the user should hear without moving focus. SC 1.3.1 covers the structural naming, and SC 1.4.1 applies if the summary row is distinguished only by colour.

Where each summary row lives in the DOM Stacked layers showing thead, a group tbody ending in its subtotal row, a second group tbody with its subtotal, and a tfoot with the grand total. Where each summary row lives in the DOMtheadcolumn headers, including the unit: Amount (EUR)tbody — Northgroup header, data rows, then "North subtotal" as the last rowtbody — Southgroup header, data rows, then "South subtotal" as the last rowtfoot"Total" row header; grand total cells; placed after the bodies in source
Subtotals stay inside their group so they inherit its rowgroup header; the grand total sits in tfoot.

When to use tfoot — and when not to

Permalink to "When to use tfoot — and when not to"

Use <tfoot> for the grand total and for any row that summarises the whole table (count, average). It gives the row a structural identity that some readers expose (“table footer”) and that styling and sorting code can target reliably.

Keep subtotals inside each group’s <tbody>, as its last row. A subtotal in the footer loses its group; a subtotal in its group inherits the group header, so it is announced as “North, North subtotal, Amount, 1,610.00”.

Do not put a total at the top of the table without saying so. Finance tables often lead with the total for emphasis. That is legitimate, but reading order then presents the total before the rows that produce it; name it clearly (“Total of 12 invoices”) so a user who meets it first knows what it is.

The misapplication to name is a summary row whose first cell is empty, with the total in the last column. With no row header, “1,700.00” is announced exactly like a data value.

Annotated code example

Permalink to "Annotated code example"
<table>
  <caption>Open invoices by region</caption>
  <thead>
    <tr><th scope="col">Invoice</th><th scope="col">Customer</th><th scope="col">Amount (EUR)</th></tr>
  </thead>
  <tbody>
    <tr><th scope="rowgroup" colspan="3">North</th></tr>
    <tr><th scope="row">INV-1042</th><td>Northwind</td><td>1,280.00</td></tr>
    <tr><th scope="row">INV-1051</th><td>Litware</td><td>330.00</td></tr>
    <!-- SC 1.3.1: subtotal inside its group, named by a row header -->
    <tr class="subtotal">
      <th scope="row" colspan="2">North subtotal</th>
      <td>1,610.00</td>
    </tr>
  </tbody>
  <!-- SC 1.3.1 + 1.3.2: grand total in tfoot, after the bodies in source -->
  <tfoot>
    <tr>
      <th scope="row" colspan="2">Total, 3 invoices</th>   <!-- names the aggregate -->
      <td>1,700.00</td>
    </tr>
  </tfoot>
</table>
<p role="status" class="visually-hidden" id="totals-status"></p>   <!-- SC 4.1.3 -->
// After an edit or filter changes the data: one message, only if the total changed
function refreshTotals(rows) {
  const total = rows.reduce((s, r) => s + r.amount, 0);
  const before = state.total;
  state.total = total;
  renderTotals(total, rows.length);
  if (before !== undefined && before !== total) {
    document.getElementById('totals-status').textContent =
      `Total now ${format(total)} euros across ${rows.length} invoices.`;
  }
}

The announcement fires only when the value changes. Editing a customer name does not change the total, and a message saying “Total now 1,700.00” when it was already 1,700.00 teaches users to ignore the region.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Event Expected announcement AT-specific deviations
Move to total amount cell “Total, 3 invoices, Amount (EUR), 1,700.00” JAWS may add “table footer” on entering tfoot
Move to subtotal cell “North, North subtotal, Amount (EUR), 1,610.00” VoiceOver omits the rowgroup header
Edit an amount Cell value, then “Total now 1,750.00 euros across 3 invoices.” Polite; follows the edit confirmation
Filter to 2 invoices Count message and total message combined Combine into one sentence to avoid a race
Sort by amount Total stays in tfoot Summary rows must be excluded from sorting
A total row, unnamed and named Comparison of a total row with an empty first cell against one with a row header naming the aggregate and count. A total row, unnamed and named✗ Unnamed summary rowFirst cell empty, amount in boldRead as "Amount, 1,700.00"Indistinguishable from a data row by earSorted along with the data if in tbody✓ Named summary rowRead as "Total, 3 invoices, Amount, 1,700.00"Lives in tfoot, excluded from sortAnnounces its new value when data changes
The only difference is a th in the first cell — and it changes the total from a mystery number into an answer.

Integration context

Permalink to "Integration context"

When a filter changes the rows, the result count and the new total describe the same event. Write them as one status message — “4 invoices shown. Total 2,140.00 euros.” — rather than two regions racing; the count half follows announcing filter result counts with aria-live.

Number formatting matters most in totals: a negative total shown in red parentheses is the classic failure described in formatting numbers and units in table cells.

A filter change, counted and totalled once Flow of a filter change: rows filtered, count and total recomputed, compared with the previous total, combined into one sentence, and announced. A filter change, counted and totalled onceFilter appliedrows changeRecomputecount and totalComparetotal changed?Combine"4 invoices shown.Total 2,140.00."Announceonce, politely
One user action, one sentence — the count and the total describe the same event.

Gotchas

Permalink to "Gotchas"

Totals computed from visible rows only. In a paginated table, a footer total of the current page is not the total of the data set. Say which: “Page total” and “Total of all 1,240 invoices” are different rows.

Averages and empty cells. An average that silently excludes “Not applicable” cells should say so in its header — “Average of 9 regions”.

Virtualised grids. If the grid renders only visible rows, the footer must still be rendered and focusable; users need to reach it without scrolling through every row.

Testing checklist

Permalink to "Testing checklist"

FAQ

Permalink to "FAQ"
How do screen readers know a row is a total?

From its row header. A th scope=“row” containing “Total” makes every cell in the row announce “Total” before its value. Bold text or a background colour on its own conveys nothing to a screen reader.

Should the total row go in tfoot?

The grand total, yes, placed after the tbody elements so reading order matches the visual order. Group subtotals belong inside each group’s tbody as its last row, so they inherit the group header.

Should a changed total be announced?

Yes, once, when an edit or filter changes its value — through the table’s status region, and combined with any count message from the same action. Do not announce when the value is unchanged.

What if the total is shown above the table?

That is fine if it is labelled clearly, for example “Total of 12 invoices: 1,700.00 euros”, so a user who reaches it first knows it summarises the rows that follow.

Permalink to "Related"

← Back to Row Grouping & Totals in Data Tables