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.
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 |
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.
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.
Related
Permalink to "Related"- Group header rows — the groups subtotals belong to
- Numbers & units in cells — formatting the totals themselves
- Announcing filter result counts — the message totals share