Skip to main content

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 scope attribute 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>
MeaningDataA label for data
Default lookNormal text, left-alignedBold, centered
Screen readersRead as a valueAnnounced as the heading for related cells
Can have scopeNoYes
index.html
<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>

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.

<tr>
<td><strong>City</strong></td>
<td><strong>Temperature (Β°C)</strong></td>
</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.

The rule of thumb

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.

index.html
<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>

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.

ValueMeaningUse for
colLabels the cells below it in its columnColumn headers
rowLabels the cells to the right of it in its rowRow headers
colgroupLabels a whole group of columnsHeaders over a <colgroup> (advanced)
rowgroupLabels a whole group of rowsHeaders 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>.

Simple rule

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.

index.html
<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>

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:

OptionCodeWhen 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.

style.css
th {
background: #e2e8f0;
text-align: left;
}

thead th {
background: #0f172a;
color: #ffffff;
}

th[scope="row"] {
font-weight: 600;
background: #f8fafc;
}
GoalCSS
Left-align headings (instead of centered)th { text-align: left; }
Right-align a numeric column's headingth.num { text-align: right; }
Different style for row headersth[scope="row"] { ... }
Different style for column headersth[scope="col"] { ... }
Keep headings visible while scrollingthead 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​

index.html
<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>

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​

  1. Bold <td> cells instead of <th>. Looks right, means nothing.
  2. Using <th> just to make text bold or centered in cells that are not labels. Use CSS.
  3. Leaving out scope. It is easy to add and removes guesswork.
  4. Wrong scope value, such as scope="row" on a top-row heading.
  5. Empty <th> cells. An empty header has no name. Use <td> for an empty corner, or a label.
  6. Vague headings like "Value" or "Info."
  7. Very long headings, repeated for every cell. Shorten them, or use abbr.
  8. No heading row at all. Every data table needs labels.
  9. Putting units in every cell instead of the heading.
  10. Relying on color alone to tell headings from data. The element itself carries the meaning.

Practice Time​

Try these in your editor:

  1. Build a table with a header row using <th scope="col">.
  2. Add a first column of row headers using <th scope="row">.
  3. Leave the top-left cell empty, then give it a label instead, and compare.
  4. Style th so it is left-aligned with a colored background.
  5. Style th[scope="row"] differently from th[scope="col"].
  6. Add a unit to a column heading, and remove it from the cells.
  7. Add abbr to a long heading.
  8. Make the header row stay visible with position: sticky.
  9. Rebuild a table that uses bold <td> headers so it uses <th> and scope.
  10. 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 and scope="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.