HTML Table Headings
Imagine someone hands you a page of numbers with no labels: 28, 31, 30, 29. What are they? Temperatures? Ages? Scores? The numbers mean nothing until you know what they describe. In a table, headings give every cell its meaning.
Sighted readers glance up at the top row, or left at the first column, and understand a cell at once. A screen reader user cannot glance. They depend on your HTML to say which heading belongs to which cell. That is the job of the <th> element and its scope attribute. In this lesson, you will learn to use them correctly, a skill that makes the biggest single difference in table accessibility.
What You Will Learnβ
- What
<th>is, and how it differs from<td> - Column headers and row headers
- How the
scopeattribute links headings to cells - How to handle the empty corner cell
- How to shorten long headings with
abbr - How to style headings
- Why a bold
<td>is not a heading
What Is a <th>?β
<th> stands for table header cell. It sits in a row just like a <td>, but it carries a different meaning: "this cell is a label for other cells."
<td> | <th> | |
|---|---|---|
| Meaning | Data | A label for data |
| Default look | Normal text, left-aligned | Bold, centered |
| Screen readers | Read as a value | Announced as the heading for related cells |
Can have scope | No | Yes |
- HTML Code
- Browser Output
<table>
<tr>
<th>City</th>
<th>Temperature (Β°C)</th>
</tr>
<tr>
<td>Mumbai</td>
<td>31</td>
</tr>
<tr>
<td>Cairo</td>
<td>28</td>
</tr>
</table>
| City | Temperature (Β°C) |
|---|---|
| Mumbai | 31 |
| Cairo | 28 |
When a screen reader lands on the cell "31," it can say: "Temperature in degrees Celsius, 31." That sentence exists only because you used <th>.
Why Not Just Make a <td> Bold?β
It looks the same, but it means nothing.
- Looks like a heading
- Is a heading
<tr>
<td><strong>City</strong></td>
<td><strong>Temperature (Β°C)</strong></td>
</tr>
<tr>
<th>City</th>
<th>Temperature (Β°C)</th>
</tr>
With bold <td> cells, the browser has no idea these are labels. A screen reader would read "City, Temperature" as two ordinary values, and then "Mumbai, 31" without ever connecting them to their labels. Search engines and other tools also lose the structure.
Style is CSS's job. Meaning is HTML's job. If a cell labels other cells, it is a <th>, whatever it looks like.
Column Headers and Row Headersβ
Headings can label a column (reading down) or a row (reading across).
Column Headersβ
These sit at the top of a column and describe every cell below.
βββββββββββ¬βββββββββββββ
β City β Temperatureβ β column headers (th)
βββββββββββΌβββββββββββββ€
β Mumbai β 31 β
β Cairo β 28 β
βββββββββββ΄βββββββββββββ
Row Headersβ
These sit at the start of a row and describe every cell to the right. In a table of people, for example, each person's name labels that row.
Both at Once: A Grid Tableβ
In a two-way table, such as a weekly timetable, you have both. The top row labels the days, and the left column labels the times.
- HTML Code
- Browser Output
<table>
<tr>
<td></td>
<th scope="col">Monday</th>
<th scope="col">Tuesday</th>
</tr>
<tr>
<th scope="row">09:00</th>
<td>HTML</td>
<td>CSS</td>
</tr>
<tr>
<th scope="row">11:00</th>
<td>Practice</td>
<td>Project</td>
</tr>
</table>
| Monday | Tuesday | |
|---|---|---|
| 09:00 | HTML | CSS |
| 11:00 | Practice | Project |
Now the cell "CSS" belongs to two headings at once: Tuesday and 09:00. A screen reader can announce it as "Tuesday, 09:00, CSS."
The scope Attributeβ
scope tells the browser (and assistive technology) which cells a <th> labels.
| Value | Meaning | Use for |
|---|---|---|
col | Labels the cells below it in its column | Column headers |
row | Labels the cells to the right of it in its row | Row headers |
colgroup | Labels a whole group of columns | Headers over a <colgroup> (advanced) |
rowgroup | Labels a whole group of rows | Headers over a <tbody> (advanced) |
<th scope="col">Price</th> <!-- labels everything below -->
<th scope="row">Notebook</th> <!-- labels everything to the right -->
Do I Always Need scope?β
Modern browsers can often guess: a <th> in the first row is probably a column header, and a <th> at the start of a row is probably a row header. But guessing is not guaranteed, and assistive tools differ. Adding scope removes the doubt, so it is a good habit for every <th>.
Add scope="col" to column headers and scope="row" to row headers. Leave colgroup and rowgroup for complex tables.
What a Screen Reader Hearsβ
Here is a small price table and how it sounds with correct headings.
- HTML Code
- Browser Output
<table>
<tr>
<th scope="col">Item</th>
<th scope="col">Price (USD)</th>
</tr>
<tr>
<th scope="row">Notebook</th>
<td>3.50</td>
</tr>
</table>
| Item | Price (USD) |
|---|---|
| Notebook | 3.50 |
On the cell "3.50," a screen reader might say: "Price in US dollars, Notebook, 3.50." The user did not have to remember the top row or the left column. The headings travelled with the cell.
The Empty Corner Cellβ
In a two-way table, the top-left cell has no heading to describe, since it sits above the row headers and beside the column headers. You have three options:
| Option | Code | When to use |
|---|---|---|
| Leave it empty | <td></td> | The simplest, usually fine |
| Label it | <th scope="col">Time</th> | When the row headers need a name |
| Hide a label from sight | <td><span class="sr-only">Time</span></td> | Visually empty, but still named for screen readers |
A label such as "Time" in the corner helps everyone, because it names what the row headers are. Choose it when the meaning is not obvious.
<tr>
<th scope="col">Time</th>
<th scope="col">Monday</th>
<th scope="col">Tuesday</th>
</tr>
Long Headings: The abbr Attributeβ
A screen reader repeats a heading for every cell it reads in that column. A long heading, such as "Average monthly rainfall in millimeters," becomes tiring. The abbr attribute gives assistive technology a shorter version.
<th scope="col" abbr="Rainfall">Average monthly rainfall (mm)</th>
Sighted users see the full heading. Some screen readers may announce the short "Rainfall" for each cell. Support varies, so also try to keep headings reasonably short in the first place.
Styling Headingsβ
Because <th> is a real element, you can style it directly with CSS.
th {
background: #e2e8f0;
text-align: left;
}
thead th {
background: #0f172a;
color: #ffffff;
}
th[scope="row"] {
font-weight: 600;
background: #f8fafc;
}
| Goal | CSS |
|---|---|
| Left-align headings (instead of centered) | th { text-align: left; } |
| Right-align a numeric column's heading | th.num { text-align: right; } |
| Different style for row headers | th[scope="row"] { ... } |
| Different style for column headers | th[scope="col"] { ... } |
| Keep headings visible while scrolling | thead th { position: sticky; top: 0; } |
Notice how the scope attribute doubles as a styling hook, a nice bonus of using it.
Headings in Different Layoutsβ
1. Headings Across the Top (Most Common)β
<tr>
<th scope="col">Name</th>
<th scope="col">Role</th>
</tr>
2. Headings Down the Sideβ
For data where each row is a "thing" with properties, the first cell of each row can be a row header.
<tr>
<th scope="row">Name</th>
<td>Aisha Khan</td>
</tr>
<tr>
<th scope="row">Role</th>
<td>Developer</td>
</tr>
3. Headings on Both Sidesβ
The grid table you built above: scope="col" along the top, and scope="row" down the left.
4. Headings That Span Columnsβ
When a heading covers several columns (for example "Q1" over "Jan, Feb, Mar"), you merge cells with colspan. You will learn that in Colspan and Rowspan, and the accessible way to label such tables in Table Accessibility.
Putting It Together: A Report Cardβ
- HTML Code
- Browser Output
<table>
<tr>
<th scope="col">Student</th>
<th scope="col">Math</th>
<th scope="col">Science</th>
<th scope="col">English</th>
</tr>
<tr>
<th scope="row">Aisha</th>
<td>92</td>
<td>88</td>
<td>95</td>
</tr>
<tr>
<th scope="row">Mateo</th>
<td>85</td>
<td>91</td>
<td>79</td>
</tr>
<tr>
<th scope="row">Linh</th>
<td>97</td>
<td>94</td>
<td>90</td>
</tr>
</table>
| Student | Math | Science | English |
|---|---|---|---|
| Aisha | 92 | 88 | 95 |
| Mateo | 85 | 91 | 79 |
| Linh | 97 | 94 | 90 |
Each score now knows its student and its subject. "Mateo, Science, 91" is what a screen reader user hears.
Global Audience Notesβ
- Write headings in the language of the page, and keep them short and plain. Translation tools handle short labels best.
- Put units in the heading, such as "Temperature (Β°C)" or "Price (USD)," instead of repeating them in every cell.
- Be specific. "Date of birth" is clearer than "Date," and "Amount (EUR)" is clearer than "Amount."
- In right-to-left languages, the column order flips when
dir="rtl"is set, and headings should follow.
Common Mistakes to Avoidβ
- Bold
<td>cells instead of<th>. Looks right, means nothing. - Using
<th>just to make text bold or centered in cells that are not labels. Use CSS. - Leaving out
scope. It is easy to add and removes guesswork. - Wrong
scopevalue, such asscope="row"on a top-row heading. - Empty
<th>cells. An empty header has no name. Use<td>for an empty corner, or a label. - Vague headings like "Value" or "Info."
- Very long headings, repeated for every cell. Shorten them, or use
abbr. - No heading row at all. Every data table needs labels.
- Putting units in every cell instead of the heading.
- Relying on color alone to tell headings from data. The element itself carries the meaning.
Practice Timeβ
Try these in your editor:
- Build a table with a header row using
<th scope="col">. - Add a first column of row headers using
<th scope="row">. - Leave the top-left cell empty, then give it a label instead, and compare.
- Style
thso it is left-aligned with a colored background. - Style
th[scope="row"]differently fromth[scope="col"]. - Add a unit to a column heading, and remove it from the cells.
- Add
abbrto a long heading. - Make the header row stay visible with
position: sticky. - Rebuild a table that uses bold
<td>headers so it uses<th>andscope. - Bonus: turn on a screen reader and listen to your table cell by cell.
Quick Recapβ
Click a question to check your answer.
1. What is the difference between <th> and <td>?
<th> labels other cells and is announced as a heading. <td> holds data. <th> is bold and centered by default.
2. Why is a bold <td> not a good heading?
It only looks like a heading. Browsers and screen readers do not know it labels other cells, so the connection between headings and data is lost.
3. What does scope="col" mean?
The <th> labels the cells below it in its column. scope="row" labels the cells to the right of it in its row.
4. How do you treat the empty top-left cell in a grid table?
Leave it as an empty <td>, or give it a <th> label such as "Time" when the row headers need a name.
5. What does the abbr attribute do on a <th>?
It supplies a shorter version of a long heading for assistive technology that repeats the heading for each cell.
Key Takeawaysβ
<th>marks a cell as a label.<td>marks it as data. Meaning belongs in the HTML, and looks belong in the CSS.- Use
scope="col"for column headers andscope="row"for row headers. - Headings travel with every cell, so a screen reader user always knows what a value means.
- Keep headings short, specific, and unit-aware, and never leave a
<th>empty. - Style headings with CSS, using
th[scope]as a handy hook.
Every table needs a heading, but it also needs a title of its own. Learn how to give it one in Caption, or return to the Tables overview.