Lesson 10 of 20

HTML Tables

What a Table Is For

A table presents tabular data — information where each value belongs to both a row and a column at the same time. A timetable, a marks sheet, a price list, a set of technical specifications: in every case, one cell means nothing on its own. "85" is meaningless; "Rahul's mark in Physics is 85" is information, and it comes from knowing the row and the column together.

That two-way relationship is the test for whether you need a table. If your data is genuinely a grid, a table is not just acceptable, it is the correct and accessible choice — and people who avoid tables entirely because they have heard "tables are bad" end up building worse alternatives out of nested divs. If your data is a simple sequence with one dimension, you wanted a list.

A table is built from rows. You write <tr> for each row, and inside it one cell for each column, in order. There is no column element you fill in separately — the columns exist only because every row has its cells in the same order. Get one row's cell count wrong and the whole grid shifts from that point down.

Example
<table>
  <caption>Internal assessment marks, Semester 4</caption>
  <thead>
    <tr>
      <th scope="col">Roll number</th>
      <th scope="col">Name</th>
      <th scope="col">Physics</th>
      <th scope="col">Chemistry</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CS-118</td>
      <td>Rahul Menon</td>
      <td>85</td>
      <td>78</td>
    </tr>
    <tr>
      <td>CS-119</td>
      <td>Ananya Sharma</td>
      <td>91</td>
      <td>88</td>
    </tr>
  </tbody>
</table>
  • <table> — wraps the whole table
  • <caption> — the table's title; must be the first child of <table>
  • <thead>, <tbody>, <tfoot> — header, body and footer row groups
  • <tr> — one table row
  • <th> — a header cell, labelling a column or a row
  • <td> — an ordinary data cell

th, scope, and Why Header Cells Are Not Just Bold Text

A <th> is a header cell — it labels the column or the row it introduces. Browsers render it bold and centred by default, which is why beginners often skip it and use a <td> with bold styling instead. On screen the two look nearly identical. To a screen reader they are completely different, and the difference decides whether the table is usable at all.

Here is why. A sighted reader who lands on the cell containing 85 glances up to the column heading and left to the row heading, and instantly knows it is Rahul's Physics mark. A screen reader user cannot glance anywhere; they hear one cell at a time. So the software reads the associated header cells out along with the value — "Physics, Rahul Menon, 85" — which reconstructs the same information. If your headers are bold <td> elements, there are no associated headers, and the user hears a stream of bare numbers with no idea what any of them refer to.

The scope attribute removes any ambiguity about which direction a header applies in. scope="col" means this cell heads its column; scope="row" means it heads its row. In a simple table the browser can often guess correctly, but tables with headers down the left side as well as across the top are exactly where guessing fails, and it costs you eleven characters to be explicit.

Example
<!-- Headers on both axes, each with an explicit scope -->
<table>
  <caption>Weekly lab schedule</caption>
  <thead>
    <tr>
      <td></td>
      <th scope="col">Monday</th>
      <th scope="col">Wednesday</th>
      <th scope="col">Friday</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">10:00</th>
      <td>Electronics</td>
      <td>Free</td>
      <td>Electronics</td>
    </tr>
    <tr>
      <th scope="row">14:00</th>
      <td>Programming</td>
      <td>Programming</td>
      <td>Free</td>
    </tr>
  </tbody>
</table>

<!-- Wrong: looks the same, carries no header information -->
<tr>
  <td><strong>Monday</strong></td>
</tr>
Notes
  • The empty <td></td> in the top-left corner of the schedule above is deliberate. That cell heads nothing, so it should not be a <th> — but it must exist, or every header in that row shifts one column to the left.

Captions and Row Groups

<caption> gives the table a title, and it must be the very first thing inside <table> — before <thead>, before any row. Put it anywhere else and it is invalid, and the browser will move it. A caption is more useful than it looks: a screen reader user listing the tables on a page sees the captions, so "Internal assessment marks, Semester 4" tells them which table they want without exploring it cell by cell. A heading above the table is not the same thing, because nothing formally connects it to the table.

<thead>, <tbody> and <tfoot> group your rows into header, body and footer. They are optional — a table of plain <tr> elements works — but they earn their place. Browsers can repeat the <thead> at the top of each page when a long table is printed, CSS can scroll a long <tbody> while the header stays put, and your markup becomes far easier to read.

<tfoot> is for summary rows: totals, averages, counts. Write it after <tbody> in the source. A table may have only one <thead> and one <tfoot>, but it can have several <tbody> groups, which is a neat way to divide a long table into labelled blocks.

Example
<table>
  <caption>Club expenses, March 2026</caption>
  <thead>
    <tr>
      <th scope="col">Item</th>
      <th scope="col">Amount (Rs)</th>
    </tr>
  </thead>
  <tbody>
    <tr><td>Sensors</td><td>2400</td></tr>
    <tr><td>Motor driver boards</td><td>1800</td></tr>
    <tr><td>Printing and stationery</td><td>650</td></tr>
  </tbody>
  <tfoot>
    <tr>
      <th scope="row">Total</th>
      <td>4850</td>
    </tr>
  </tfoot>
</table>
Notes
  • Numbers are easier to compare when they are right-aligned, because the digits line up in columns. That is a CSS job: td.amount { text-align: right; font-variant-numeric: tabular-nums; }.

Merging Cells with colspan and rowspan

colspan makes a cell stretch across several columns; rowspan makes it stretch down several rows. Both take a number — the total number of cells the merged cell occupies, including its own position, so colspan="2" covers two columns, not two extra ones.

The rule people get wrong is what happens to the cells that were covered. They must be deleted, not left in place. If a cell in row three spans two columns, that row now has one fewer <td> than the others. Leave the covered cell in and you have pushed an extra cell past the right edge of the table, which browsers handle by silently widening the whole grid — so you get a mysterious empty column running down the entire table.

Use merged cells sparingly. Every merge makes the grid harder for assistive technology to interpret and harder for you to maintain when a row is added later. A table that needs merges in several directions is usually two simpler tables, and splitting it is nearly always the better answer.

Example
<table>
  <caption>Techfest schedule</caption>
  <tbody>
    <tr>
      <th scope="col">Time</th>
      <th scope="col">Hall A</th>
      <th scope="col">Hall B</th>
    </tr>
    <tr>
      <th scope="row">09:00</th>
      <!-- One session fills both halls: 2 columns, 1 cell -->
      <td colspan="2">Inauguration</td>
    </tr>
    <tr>
      <th scope="row">10:00</th>
      <!-- This workshop runs across two time slots -->
      <td rowspan="2">Robotics workshop</td>
      <td>Coding contest</td>
    </tr>
    <tr>
      <th scope="row">11:00</th>
      <!-- No cell for Hall A here: the rowspan above covers it -->
      <td>Quiz</td>
    </tr>
  </tbody>
</table>
Notes
  • If your table suddenly grows an extra empty column, count the cells in each row. Almost always one row has a colspan or rowspan plus the cell it was supposed to replace.

Never Use Tables for Layout

In the late 1990s CSS could not lay out a page, so designers built page layouts out of invisible tables — a table for the header, nested tables for the sidebar and the content column, spacer images to hold the gaps open. You will still find this in very old code and in HTML email templates, where the rules are genuinely different. On a normal web page it has been the wrong approach for two decades.

The reasons are practical, not stylistic. A screen reader announces a table and reads it as a grid, so a layout table tells a blind user that your navigation menu and your article are related data in a spreadsheet — pure noise. A table layout cannot rearrange itself for a phone, because a table's grid is fixed by its markup; on a narrow screen it either overflows or crushes. And nesting tables three deep to position a sidebar produces markup that nobody, including you next month, can safely edit.

CSS Grid and Flexbox exist specifically to do this job, and they do it in a handful of lines while adapting to any screen size. The rule is easy to remember: use a table when your content is a table, and never to put things next to each other.

Real data tables do have one genuine problem on phones — a table with six columns simply does not fit. The standard fix is not to abandon the table but to wrap it in a container that scrolls sideways, so the grid stays intact and the visitor swipes to see the rest. Modern styling of tables is also entirely a CSS job: the old border, cellpadding, cellspacing, align and bgcolor attributes are obsolete.

Example
<!-- Wrong: a table used to place things side by side -->
<table>
  <tr>
    <td><nav>Menu</nav></td>
    <td><article>Main content</article></td>
  </tr>
</table>

<!-- Right: CSS does the layout -->
<div class="page">
  <nav>Menu</nav>
  <article>Main content</article>
</div>

.page {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 24px;
}

/* A wide data table that scrolls instead of overflowing */
.table-scroll {
  overflow-x: auto;
}
  • Use a table when each value belongs to a row and a column at once
  • Use CSS Grid or Flexbox for anything to do with page layout
  • Style tables with CSS; border, cellpadding, cellspacing, align and bgcolor are obsolete attributes
  • border-collapse: collapse is what turns doubled-up borders into single clean lines
  • Wrap wide data tables in a container with overflow-x: auto so they scroll on small screens
Build an Accessible Data Table
HTML
<h2>Internal Assessment Marks</h2>

<div class="table-scroll">
  <table>
    <caption>Semester 4 — Section A</caption>
    <thead>
      <tr>
        <th scope="col">Roll number</th>
        <th scope="col">Name</th>
        <th scope="col">Physics</th>
        <th scope="col">Chemistry</th>
        <th scope="col">Total</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">CS-118</th>
        <td>Rahul Menon</td>
        <td>85</td>
        <td>78</td>
        <td>163</td>
      </tr>
      <tr>
        <th scope="row">CS-119</th>
        <td>Ananya Sharma</td>
        <td>91</td>
        <td>88</td>
        <td>179</td>
      </tr>
      <tr>
        <th scope="row">CS-120</th>
        <td>Imran Qureshi</td>
        <td>74</td>
        <td>82</td>
        <td>156</td>
      </tr>
    </tbody>
    <tfoot>
      <tr>
        <th scope="row" colspan="4">Class average</th>
        <td>166</td>
      </tr>
    </tfoot>
  </table>
</div>

<p>Try removing a &lt;td&gt; from one row and run it again — watch the whole
   grid shift from that point down.</p>
CSS
body {
  font-family: system-ui, Arial, sans-serif;
  padding: 24px;
}
.table-scroll {
  overflow-x: auto;
}
table {
  border-collapse: collapse;
  width: 100%;
  min-width: 480px;
}
caption {
  text-align: left;
  font-weight: 600;
  padding-bottom: 8px;
  color: #555;
}
th, td {
  border: 1px solid #ddd;
  padding: 10px 12px;
  text-align: left;
}
thead th {
  background: #f4f4f6;
}
tbody tr:nth-child(even) {
  background: #fafafa;
}
tfoot th, tfoot td {
  font-weight: 600;
  background: #f4f4f6;
}
Notes
  • border-collapse: collapse is worth remembering as the first line of any table stylesheet. Without it, each cell draws its own border and you get a doubled, slightly gappy grid that looks unfinished.
Ask AI