How to Give Every Table Value a Meaningful Header
Use <th> with row or column scope for simple tables, grouped scope for structural groups, and id/headers only for irregular relationships.

For accessible column and row headers in HTML tables, mark cells that label data as <th> and ordinary values as <td>. Use scope="col" and scope="row" for regular tables. Use scope="colgroup" or scope="rowgroup" when headers apply to structurally defined groups. Reserve unique id values and headers references for relationships that those scope values cannot describe accurately. If the hierarchy becomes difficult to explain or maintain, simplify or split the table.
Related: Spreadsheet Data Dictionary Template.
The short answer: match the markup to the header relationship
Choose the least complex technique that accurately describes the table:
| Header relationship | Recommended markup |
|---|---|
| Ordinary column label | <th scope="col"> |
| Ordinary row label | <th scope="row"> |
| Header for a defined column group | <th scope="colgroup"> |
| Header for a defined row group | <th scope="rowgroup"> |
| Relationship not expressible with row, column, row-group, or column-group scope | Unique header id values plus headers references |
The core distinction is semantic: a cell that labels other cells is a <th>, while an ordinary value is a <td>. Assistive technologies can use this structure to determine which labels apply to a value, according to the W3C Web Accessibility Initiative’s tables tutorial.
Bold text, a shaded background, or placement in the first row or column may help sighted readers recognize a label. Those visual cues do not create a programmatic header association. Keep useful styling, but apply it to meaningful HTML.
Use a practical test when reviewing a published table: choose any data value and identify its applicable row label, column label, and group labels. The answer should be unambiguous without depending solely on color, indentation, or visual position.
Explicit scope makes a header’s direction clear and is a useful convention for publication-ready tables. That does not mean every <th> in every simple table universally requires a scope attribute; some straightforward relationships can be inferred. The objective is a clear and accurate association, not the mechanical addition of attributes.
Copy-ready pattern for a table with column and row headers
A regular two-dimensional dataset usually needs a top row of column headers and a first column of row headers. This complete example uses synthetic public-project totals:
<table>
<caption>Quarterly public-project totals by region</caption>
<thead>
<tr>
<th scope="col">Region</th>
<th scope="col">Q1 projects</th>
<th scope="col">Q2 projects</th>
<th scope="col">Q3 projects</th>
<th scope="col">Q4 projects</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">North region</th>
<td>12</td>
<td>15</td>
<td>14</td>
<td>18</td>
</tr>
<tr>
<th scope="row">South region</th>
<td>9</td>
<td>11</td>
<td>13</td>
<td>16</td>
</tr>
</tbody>
</table>
In this table, 12 is an ordinary value, so it appears in a <td>. Its meaning comes from two directions:
<th scope="row">North region</th>identifies its row.<th scope="col">Q1 projects</th>identifies its column.
Together, those headers give the value its full meaning: 12 Q1 projects in the North region. Semantic markup allows assistive technologies to establish that context instead of requiring users to reconstruct it from visual coordinates.
The <caption> appears immediately after the opening <table> tag and describes the table as a whole. Here, it tells readers that the dataset contains quarterly public-project totals organized by region. It does not replace the row and column headers that explain individual values. MDN’s HTML table accessibility guidance likewise distinguishes the table-level caption from cell-level header associations.
Treat the top-left intersection according to its meaning. In this example, Region genuinely labels the first column, so it is a column header. In another table, that position might not supply a label. Do not make it a header merely because it appears in the header row, and do not assume every table needs the same top-left-cell pattern.
This distinction matters when converting a spreadsheet. A blank corner created by merged spreadsheet headings may be a formatting artifact rather than a meaningful part of the data model. Resolve its purpose before generating the HTML.
Use grouped scope when a header spans columns or rows
Some tables need a header that applies to several related columns or rows. The colspan and rowspan attributes describe how many grid cells a header occupies. For grouped scope, the corresponding group must also be represented in the table structure.
Many layered headers remain regular grouped relationships. A table does not require id and headers merely because it has two header levels.
Grouping related columns
Suppose a publication compares planned and actual revenue with planned and actual costs:
<table>
<caption>Planned and actual amounts by program</caption>
<colgroup span="1"></colgroup>
<colgroup span="2"></colgroup>
<colgroup span="2"></colgroup>
<thead>
<tr>
<th rowspan="2" scope="col">Program</th>
<th colspan="2" scope="colgroup">Revenue</th>
<th colspan="2" scope="colgroup">Costs</th>
</tr>
<tr>
<th scope="col">Planned</th>
<th scope="col">Actual</th>
<th scope="col">Planned</th>
<th scope="col">Actual</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Transit renewal</th>
<td>240</td>
<td>255</td>
<td>180</td>
<td>192</td>
</tr>
</tbody>
</table>
The first <colgroup> represents the program-name column. The next two elements each define a two-column group. The first-level Revenue and Costs headers use both colspan="2" and scope="colgroup", while each second-level Planned or Actual header applies to one column.
A column group must be represented before scope="colgroup" can identify it. Adding grouped scope to a spanning header without corresponding <colgroup> structure does not create the missing group. The W3C guidance for tables with irregular headers demonstrates this combination of column-group structure, spanning headers, and individual column headers.
Grouping related rows
A row-group header follows the same principle, but its group is formed by a table section:
<table>
<caption>Regional office results</caption>
<thead>
<tr>
<th scope="col">Region</th>
<th scope="col">Office</th>
<th scope="col">Completed projects</th>
</tr>
</thead>
<tbody>
<tr>
<th rowspan="2" scope="rowgroup">Europe</th>
<th scope="row">Berlin</th>
<td>24</td>
</tr>
<tr>
<th scope="row">Lisbon</th>
<td>17</td>
</tr>
</tbody>
<tbody>
<tr>
<th rowspan="2" scope="rowgroup">Americas</th>
<th scope="row">Toronto</th>
<td>21</td>
</tr>
<tr>
<th scope="row">Lima</th>
<td>19</td>
</tr>
</tbody>
</table>
Each <tbody> defines a separate row group. Europe and Americas label their respective groups, while each office remains the row header for its own result. Row groups can be represented by <thead>, <tbody>, and <tfoot>, and multiple <tbody> elements can define separate body groups.
Do not add scope="rowgroup" merely because a label spans multiple rows. Confirm that the rows form an actual structural group. Likewise, use scope="colgroup" only when the corresponding columns have been grouped.
Reserve id and headers for genuinely irregular associations
Use explicit id and headers associations when the applicable labels cannot be represented accurately through ordinary row, column, row-group, or column-group relationships.
The method has two parts:
- Give each applicable
<th>a uniqueid. - Add a
headersattribute to the associated<td>or<th>, listing the relevant IDs separated by spaces.
Here is a coherent layered example:
<table>
<caption>Selected regional metrics by year</caption>
<thead>
<tr>
<th id="area">Area</th>
<th id="metric">Metric</th>
<th id="year-2025">2025</th>
<th id="year-2026">2026</th>
</tr>
</thead>
<tbody>
<tr>
<th id="region-eu" rowspan="2">Europe</th>
<th id="metric-sales">Sales</th>
<td headers="region-eu metric-sales year-2025">125</td>
<td headers="region-eu metric-sales year-2026">138</td>
</tr>
<tr>
<th id="metric-returns">Returns</th>
<td headers="region-eu metric-returns year-2025">8</td>
<td headers="region-eu metric-returns year-2026">6</td>
</tr>
</tbody>
</table>
The value 125 references three applicable headers:
region-euforEuropemetric-salesforSalesyear-2025for2025
In this version of the markup, the parent area label is not encoded as a separate row group, so ordinary row and column scope alone would not fully express the layered relationship. Explicit references identify the complete set directly. If every area can instead be placed in its own <tbody>, restructuring the table around scope="rowgroup" may produce simpler markup.
Every token in a headers value must match the unique id of an applicable <th>. A data cell can reference multiple header cells, and the order of the space-separated IDs does not change which headers are associated with it:
<td headers="region-eu metric-sales year-2025">125</td>
<td headers="year-2025 metric-sales region-eu">125</td>
Keep a consistent order anyway. It makes the source easier for authors and reviewers to inspect.
Explicit references are precise but create maintenance work. Renaming an ID without updating its references breaks the intended connection. Duplicate IDs make references ambiguous, and copied rows can retain links to the wrong headers. This is why id and headers should be an exception rather than a default for ordinary tables.
If many cells require long lists of IDs, reconsider the source hierarchy. Separate tables for different measures, periods, or populations may be easier to understand and maintain.
Know what captions and table sections do—and do not do
A <caption> is a concise, visible name or description for the entire table. It belongs immediately after the opening <table> tag:
<table>
<caption>Quarterly public-project totals by region</caption>
<!-- Table rows follow -->
</table>
A good caption helps readers determine what the table contains. It does not associate 12 with North region or Q1 projects; cell-level meaning still comes from the applicable header cells.
The section elements <thead>, <tbody>, and <tfoot> organize rows into structural groups. They can also provide useful hooks for styling. They do not independently turn cells into headers or create value-to-header associations.
For example, placing a row of <td> cells inside <thead> does not make those cells semantic column headers. If they label columns, they should be <th> elements with an appropriate association method.
Conversely, <th> is not limited to the first row. It can label data elsewhere in a table, and in a layered structure one <th> can be associated with another header. For tables with content that is easy to distinguish, the W3C advises marking up header cells with <th> and data cells with <td> elements (Tables Tutorial).
Do not use the obsolete summary attribute. Put a concise public-facing description in <caption>, provide genuinely necessary context in nearby page content, and simplify structures that require extensive navigation instructions.
Prepare and inspect spreadsheet-derived tables before publication
A spreadsheet can communicate hierarchy through merged cells, indentation, fill colors, borders, or blank space. Those conventions may be understandable to the file’s author, but they are not substitutes for HTML semantics. Table markup can also be lost when content is converted between formats, so inspect the final rendered HTML rather than assuming that the spreadsheet’s meaning survived export or CMS processing, as the W3C tables tutorial cautions.
Use this pre-publication checklist:
- Replace blank or ambiguous headings. A heading such as
Totalmay need to becomeTotal projects,Total expenditure, or another specific label. - Expand unclear abbreviations. Readers should not have to guess what
Act.,Var., or an internal department code means. - Identify genuine row and column labels. Convert those cells to
<th>rather than styling<td>elements to resemble headers. - Flatten unnecessary merged cells. Repeated explicit labels are often easier to publish than an elaborate visual hierarchy.
- Choose the appropriate association method. Use grouped or explicit associations only when the table’s structure requires them.
- Add a concise caption. Describe the subject of the whole table rather than every field.
- Inspect the rendered HTML. Review the output readers receive, not only the spreadsheet, template, or CMS editor.
Then perform a cell-by-cell audit. Select representative values from the start, middle, and end of the table and answer:
- What is this value’s row header?
- What is its column header?
- Does a column-group or row-group header also apply?
- Can those relationships be determined from the markup rather than color, indentation, or visual position alone?
If the table uses headers, verify manually that every referenced ID exists, is unique within the page, and belongs to an applicable <th>. If it uses scope="colgroup" or scope="rowgroup", confirm that the corresponding column or row groups are present in the rendered structure.
Automated validation can assist the review, but passing a checker does not prove that labels are meaningful or that the correct headers apply to every value. Human inspection remains necessary.
This audit is part of preparing structured data for public web publication, whether the destination is a hand-coded article, a newsroom data page, or a spreadsheet-publishing service. When a table is embedded or transformed for a final presentation, inspect that final view rather than assuming the source markup still describes what readers receive.
Mark real labels as <th>, use row and column scope for regular tables, add grouped scope only when the corresponding structure exists, and use id and headers only for relationships that scope cannot express. Before publishing, inspect several rendered values and confirm that each retains clear row, column, and group context. If doing so requires excessive markup, simplify or divide the table.