Lesson 12 / 25
Tables
Table elements' structure, header cells' scope, building the grid with colspan and rowspan, and computing a data cell's headers.
Contents
The previous lesson connected the document to other resources. This lesson takes up the topic’s last structure: two-dimensional data.
Measurement records are not a list. Every row has more than one field, every field has a heading, and a cell’s meaning comes from both its row’s and its column’s heading. The table element declares this two-dimensional relationship.
When a Table Is Used
The criterion is single: is the data two-dimensional? If every cell’s meaning comes from the intersection of a row heading and a column heading, the structure is a table.
Using table elements to build page layout is a separate subject and is not done: layout is the presentation layer’s job, and when a table element is used, a screen reader announces the content as a row-and-column relationship — a data relationship that does not exist gets declared.
Basic Structure
<table> <caption>North Slope station's weekly measurement summary</caption> <thead> <tr><th scope="col">Day</th><th scope="col">Low (°C)</th></tr> </thead> <tbody> <tr><th scope="row">Monday</th><td>-4</td></tr> <tr><th scope="row">Tuesday</th><td>-6</td></tr> </tbody> <tfoot> <tr><th scope="row">Average</th><td>-5</td></tr> </tfoot> </table>
caption is the table’s name and is written as the table’s first child. It declares what the
table shows; a screen reader announces this text when entering the table, so it names the
table outside its context. A heading written as a separate paragraph above the table does not
do this job: there is no declared bond between it and the table.
thead, tbody, and tfoot split the table into three sections. Even if tbody is not
written, the parser builds it. The reason for writing them is that in long tables the header
row is separated from the data: this separation gives information both to the presentation
layer and to tools that process the table by section.
tr is a row, th a header cell, td a data cell. The difference between th and td is
not appearance: a header cell names the data cells associated with it.
A Header’s Scope
The scope attribute declares which direction of cells a header cell names: col its column,
row its row. In simple tables, this declaration does not leave the cell–header match to
guesswork.
Column headers are found inside thead, row headers in each row’s first cell. Both can be
present at once — in the example above, Monday is a row header, Low a column header, and
the -4 cell is associated with both.
The Grid and Spanning
A table is written row by row in the source text but is really a grid. The colspan and
rowspan attributes let a cell cover more than one grid slot, and in that case the cell count
in the source text and the slot count in the grid diverge.
The script below builds the grid and computes every data cell’s headers.
// tables.mjs — builds the grid with colspan/rowspan, finds each cell's headers const rows = [ [{ h: 1, t: "Day", rowspan: 2 }, { h: 1, t: "Temperature (°C)", colspan: 2 }, { h: 1, t: "Humidity (%)", rowspan: 2 }], [{ h: 1, t: "Low" }, { h: 1, t: "High" }], [{ h: 1, t: "Monday" }, { t: "-4" }, { t: "3" }, { t: "72" }], [{ h: 1, t: "Tuesday" }, { t: "-6" }, { t: "1" }, { t: "68" }], ]; const grid = []; const place = (r, c, cell) => { grid[r] ??= []; grid[r][c] = cell; }; let rowNo = 0; for (const row of rows) { let colNo = 0; for (const cell of row) { while (grid[rowNo]?.[colNo]) colNo += 1; // skip occupied spot const width = cell.colspan ?? 1; const height = cell.rowspan ?? 1; for (let r = 0; r < height; r += 1) { for (let c = 0; c < width; c += 1) place(rowNo + r, colNo + c, cell); } colNo += width; } rowNo += 1; } console.log("--- grid ---"); for (const row of grid) { console.log(row.map((h) => (h.h ? "[" + h.t + "]" : h.t).padEnd(20)).join("").trimEnd()); } function headers(r, c) { const rowHeaders = []; for (let i = c - 1; i >= 0; i -= 1) { const h = grid[r][i]; if (h?.h && !rowHeaders.includes(h.t)) rowHeaders.unshift(h.t); } const colHeaders = []; for (let i = r - 1; i >= 0; i -= 1) { const h = grid[i][c]; if (h?.h && !colHeaders.includes(h.t)) colHeaders.unshift(h.t); } return [...rowHeaders, ...colHeaders]; } console.log("--- data cells' headers ---"); for (let r = 2; r < grid.length; r += 1) { for (let c = 0; c < grid[r].length; c += 1) { const cell = grid[r][c]; if (cell.h) continue; console.log(cell.t.padStart(3), "<-", headers(r, c).join(" / ")); } }
--- grid --- [Day] [Temperature (°C)] [Temperature (°C)] [Humidity (%)] [Day] [Low] [High] [Humidity (%)] [Monday] -4 3 72 [Tuesday] -6 1 68 --- data cells' headers --- -4 <- Monday / Temperature (°C) / Low 3 <- Monday / Temperature (°C) / High 72 <- Monday / Humidity (%) -6 <- Tuesday / Temperature (°C) / Low 1 <- Tuesday / Temperature (°C) / High 68 <- Tuesday / Humidity (%)
In the source structure, the first row carries three cells, the second row two; in the grid,
though, every row is four slots. The Day and Humidity (%) headers span two rows, the
Temperature (°C) header spans two columns, and the same cell appears in more than one slot
in the grid.
The output’s second section shows the real information the markup carries. The value -4 is
meaningless on its own; together with the three headers it is associated with, it means “the
low temperature value on Monday.” Screen readers announce these headers when a cell is
entered; this is how the user tracks where they are in the row and column.
Here is the markup’s counterpart:
<tr> <th scope="col" rowspan="2">Day</th> <th scope="colgroup" colspan="2">Temperature (°C)</th> <th scope="col" rowspan="2">Humidity (%)</th> </tr> <tr> <th scope="col">Low</th> <th scope="col">High</th> </tr>
The colgroup scope value declares that the header names not a single column but a group of
columns; its row-direction counterpart is the rowgroup value.
Explicit Association in Complex Tables
The computation above relies on adjacency and gives the correct result when headers are placed
regularly. In irregular tables — headers in the middle of the table or at more than one level —
this guess is not enough. In this case the association is declared explicitly: every header
cell is given an id, and every data cell lists the identifiers of the headers it is
associated with in the headers attribute.
<table> <tr> <th id="day" rowspan="2">Day</th> <th id="temp" colspan="2">Temperature (°C)</th> </tr> <tr> <th id="temp-low">Low</th> <th id="temp-high">High</th> </tr> <tr> <th id="day-mon" headers="day">Monday</th> <td headers="day-mon temp temp-low">-4</td> <td headers="day-mon temp temp-high">3</td> </tr> </table>
Explicit association is expensive to maintain: every cell lists its own headers, and identifiers have to be unique within the document. This cost is paid only when the structure cannot be resolved by adjacency. A better solution in most cases is splitting the table — a complex table, once split into two simple tables, becomes easier both to write and to read.
Column Groups
The colgroup and col elements are for giving attributes to columns in bulk and are written
at the start of the table, after the caption element. They carry no content; they name the
columns in the grid. Their use relates to the presentation layer and does not establish a data
association; they do not stand in for cell headers.
In the Station Document
<h2>Weekly Summary</h2> <table> <caption>North Slope station, daily extremes over the last seven days</caption> <thead> <tr> <th scope="col" rowspan="2">Day</th> <th scope="colgroup" colspan="2">Temperature (°C)</th> <th scope="col" rowspan="2">Humidity (%)</th> </tr> <tr> <th scope="col">Low</th> <th scope="col">High</th> </tr> </thead> <tbody> <tr><th scope="row">Monday</th><td>-4</td><td>3</td><td>72</td></tr> <tr><th scope="row">Tuesday</th><td>-6</td><td>1</td><td>68</td></tr> </tbody> </table>
Summary
- A table is for two-dimensional data where a cell’s meaning comes from the intersection of row and column headers; it is not used for page layout.
captionnames the table and is bound to it; a separate paragraph written above it does not establish this bond.- The difference between
thandtdis not appearance, it is function;scopedeclares which direction of cells a header names. - With
colspanandrowspan, the cell count in the source text separates from the slot count in the grid; the same cell appears in more than one slot. - In structures that cannot be resolved by adjacency, explicit association is established with
headersandid; because maintenance cost is high, splitting the table is considered first.
Next Step
This topic built the document’s skeleton: root structure, syntax, headings, text, lists, links, and tables. Everything written in the document is a structural declaration, but these declarations do not yet name the document’s sections: where does navigation end, where does the main content begin, which one is the footer? The next topic takes up the sectioning tags that answer this question and builds the document’s navigation structure.
To keep your progress and take notes, Log in
My notes
Log in to take notes.