When a Simple Table Is Not Enough
Every school notice board in India has two kinds of tables that students read with great care: the exam timetable and the marksheet. A timetable often has one date with two exam shifts, or one row that says “Study leave” across the whole width. A marksheet has headings like “Term 1” that sit above two smaller columns, and a total row at the bottom.
In the HTML Lists and Tables lesson you built simple tables with <table>, <tr>, <th> and <td>. Those are fine for neat grids where every cell is the same size. Real timetables and marksheets need more: cells that stretch across columns or rows, a title for the table, and clear sections for the header, body and total.
In this lesson you will learn the tools that make this possible, and how to keep complex tables readable for screen reader users too. Every example has a small <style> block that adds borders, so you can see the structure clearly.
caption, thead, tbody and tfoot
Start with the parts that give a table its shape:
| Element | Purpose |
|---|---|
<caption> | The title of the table. It must be the first thing inside <table>. |
<thead> | The header rows, usually the column headings. |
<tbody> | The main data rows. |
<tfoot> | Summary rows such as totals or notes. Write it after <tbody>. |
These groups do not change how the table looks by default, but they give it meaning. A screen reader can announce the caption, CSS can style each group differently, and when a long table is printed, some browsers repeat the <thead> on every page.
<style>
table { border-collapse: collapse; }
th, td { border: 1px solid #555; padding: 6px 12px; }
thead { background: #e3f2fd; }
tfoot { background: #fff8e1; font-weight: bold; }
</style>
<table>
<caption>Half-Yearly Marksheet: Rohan Verma, Class 9 B</caption>
<thead>
<tr><th>Subject</th><th>Marks (out of 100)</th></tr>
</thead>
<tbody>
<tr><td>English</td><td>78</td></tr>
<tr><td>Hindi</td><td>84</td></tr>
<tr><td>Mathematics</td><td>91</td></tr>
<tr><td>Science</td><td>88</td></tr>
</tbody>
<tfoot>
<tr><td>Total</td><td>341</td></tr>
</tfoot>
</table>
Run it and notice the caption above the table, the blue header and the highlighted total row. Change thead in the style block to a different colour to see that the whole group changes together.
Header Cells with scope
A <th> cell is a heading, but a heading for what? In a class result table, “Maths” is the heading of a column, and a student’s name is the heading of a row. The scope attribute says which one:
scope="col": this heading describes the cells below it.scope="row": this heading describes the cells to its right.
Sighted users understand this by looking. A screen reader user cannot see the layout, so the screen reader uses scope to read a cell like “Fatima, Science, 19” instead of just “19”.
<style>
table { border-collapse: collapse; }
th, td { border: 1px solid #555; padding: 6px 12px; }
th[scope="col"] { background: #e3f2fd; }
th[scope="row"] { background: #f1f8e9; text-align: left; }
</style>
<table>
<caption>Unit Test Marks, Class 9 B</caption>
<thead>
<tr>
<th scope="col">Student</th>
<th scope="col">Maths</th>
<th scope="col">Science</th>
</tr>
</thead>
<tbody>
<tr><th scope="row">Rohan</th><td>18</td><td>17</td></tr>
<tr><th scope="row">Fatima</th><td>20</td><td>19</td></tr>
<tr><th scope="row">Karthik</th><td>16</td><td>18</td></tr>
</tbody>
</table>
colspan: One Cell Across Several Columns
The colspan attribute makes a cell stretch across more than one column. colspan="3" means “this cell takes the space of three cells”. It is perfect for a “Grand Total” label that should sit under several columns.
The important rule: every row must add up to the same number of columns. Here the table has 4 columns. The total row has one cell with colspan="3" and one normal cell, and 3 + 1 = 4.
<style>
table { border-collapse: collapse; }
th, td { border: 1px solid #555; padding: 6px 12px; text-align: center; }
</style>
<table>
<caption>Science Marks with Practicals</caption>
<thead>
<tr>
<th scope="col">Subject</th>
<th scope="col">Theory (80)</th>
<th scope="col">Practical (20)</th>
<th scope="col">Total (100)</th>
</tr>
</thead>
<tbody>
<tr><th scope="row">Physics</th><td>64</td><td>18</td><td>82</td></tr>
<tr><th scope="row">Chemistry</th><td>70</td><td>19</td><td>89</td></tr>
</tbody>
<tfoot>
<tr>
<th scope="row" colspan="3">Grand Total (out of 200)</th>
<td>171</td>
</tr>
</tfoot>
</table>
Change colspan="3" to colspan="2" and run it. The total moves to the wrong column and a gap appears at the end. This is the most common table bug.
rowspan: One Cell Down Several Rows
rowspan works the same way, but downwards. In a practical exam timetable, one date often has a morning and an afternoon exam. Instead of writing the date twice, give its cell rowspan="2".
The tricky part is the next row. Because the date cell already fills the first column of the next row, you must leave that cell out of the next row. The next row starts directly with “Afternoon”.
<style>
table { border-collapse: collapse; }
th, td { border: 1px solid #555; padding: 6px 12px; }
</style>
<table>
<caption>Practical Exam Timetable</caption>
<thead>
<tr><th scope="col">Date</th><th scope="col">Shift</th><th scope="col">Subject</th></tr>
</thead>
<tbody>
<tr>
<th scope="rowgroup" rowspan="2">Monday 1 March</th>
<td>Morning</td><td>Physics</td>
</tr>
<tr>
<td>Afternoon</td><td>Chemistry</td>
</tr>
<tr>
<th scope="rowgroup" rowspan="2">Wednesday 3 March</th>
<td>Morning</td><td>Biology</td>
</tr>
<tr>
<td>Afternoon</td><td>Computer Science</td>
</tr>
</tbody>
</table>
Here the date headings use scope="rowgroup", which tells assistive technology that the heading belongs to the group of rows it spans, not only to the first one.
Plan on paper first. Before writing a complex table, draw it in your notebook as a grid. Mark which cells span two boxes. Then count the cells in each row. Five minutes with a pencil saves an hour of confusion.
colgroup and col: Style Whole Columns
HTML builds tables row by row, so there is no column element in the body. To style a whole column at once, use <colgroup> with one <col> for each column, placed just after the caption. A <col> can cover several columns with span.
Only a few CSS properties work on columns: mainly background, width, border (when borders are collapsed) and visibility. You cannot change text colour or alignment through <col>. For those, style the cells themselves.
<style>
table { border-collapse: collapse; }
th, td { border: 1px solid #555; padding: 6px 12px; }
.marks { background: #f3e5f5; }
.total { background: #fff3e0; width: 120px; }
</style>
<table>
<caption>Term Marks, Class 9 B</caption>
<colgroup>
<col>
<col span="2" class="marks">
<col class="total">
</colgroup>
<thead>
<tr><th scope="col">Student</th><th scope="col">Term 1</th><th scope="col">Term 2</th><th scope="col">Total</th></tr>
</thead>
<tbody>
<tr><th scope="row">Rohan</th><td>341</td><td>356</td><td>697</td></tr>
<tr><th scope="row">Fatima</th><td>372</td><td>380</td><td>752</td></tr>
</tbody>
</table>
Making Tables Accessible
A screen reader reads a table cell by cell. Without good structure, a marksheet becomes a confusing list of numbers. Follow these rules:
- Give every data table a
<caption>so users know what it is before they enter it. - Use
<th>for every heading and addscope. Use<td>for data only. - Avoid empty heading cells. Write a short word like “Subject” instead.
- Keep tables as simple as possible. If a table needs many levels of spanning headers, consider splitting it into two smaller tables. For very complex tables, HTML also offers the
headersattribute, which links a cell to theidvalues of its heading cells. - Use tables only for data in rows and columns, never for page layout. Use CSS Flexbox or Grid for layout.
Real-Life Example: A Class 10 Pre-Board Timetable
Here is a full exam timetable. It uses a two-row header where “Exam Shift” spans the Morning and Afternoon columns, a study leave row that spans both shifts, an English exam that spans two days, and a footer note across the whole table. It is wrapped in a <div> with overflow-x: auto, so on a small phone the table scrolls sideways instead of breaking the page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Class 10 Pre-Board Exam Timetable</title>
<style>
body { font-family: sans-serif; padding: 16px; }
.scroll { overflow-x: auto; }
table { border-collapse: collapse; min-width: 480px; }
caption { font-weight: bold; font-size: 1.1em; margin-bottom: 8px; }
th, td { border: 1px solid #444; padding: 8px 10px; text-align: center; }
thead th { background: #1565c0; color: #fff; }
tbody th { background: #e3f2fd; }
.off { background: #eeeeee; font-style: italic; }
tfoot td { text-align: left; font-size: 0.9em; }
</style>
</head>
<body>
<div class="scroll">
<table>
<caption>Sunrise Public School: Class 10 Pre-Board Exams</caption>
<colgroup>
<col>
<col>
<col span="2">
</colgroup>
<thead>
<tr>
<th scope="col" rowspan="2">Date</th>
<th scope="col" rowspan="2">Day</th>
<th scope="colgroup" colspan="2">Exam Shift</th>
</tr>
<tr>
<th scope="col">Morning (9:30 to 12:30)</th>
<th scope="col">Afternoon (2:00 to 4:00)</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">1 March</th><td>Monday</td>
<td>Mathematics</td><td>Painting (practical)</td>
</tr>
<tr>
<th scope="row">2 March</th><td>Tuesday</td>
<td class="off" colspan="2">Study leave, no exam</td>
</tr>
<tr>
<th scope="row">3 March</th><td>Wednesday</td>
<td>Science</td><td>Computer Applications</td>
</tr>
<tr>
<th scope="row">4 March</th><td>Thursday</td>
<td rowspan="2">English (two papers, one each day)</td>
<td>None</td>
</tr>
<tr>
<th scope="row">5 March</th><td>Friday</td>
<td>Hindi</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4">Report 30 minutes before the shift. Bring your admit card and a blue pen.</td>
</tr>
</tfoot>
</table>
</div>
</body>
</html>
Count the cells in the 4 March and 5 March rows. The 5 March row has only three cells, because the English cell from the row above fills its morning column.
Try these changes:
- Add a row for 8 March (Monday) with Social Science in the morning and no afternoon exam.
- Make the 1 March afternoon painting exam a full-day exam by deleting the Mathematics cell and giving Painting
colspan="2". - Give the second
<col>a class with a light background colour to highlight the Day column.
Common Mistakes (and How to Fix Them)
- Rows that do not add up. A row with too many or too few cells pushes data into the wrong column. Fix: count each row, adding the
colspanvalues, and make every total match. - Forgetting to remove cells under a rowspan. The row below gets one extra cell that sticks out on the right. Fix: delete one cell from each row that the rowspan covers.
- Putting the caption in the wrong place. A
<caption>after the rows is not valid. Fix: make it the very first child of<table>. - Using bold
<td>instead of<th>. It looks like a heading but screen readers do not know it is one. Fix: use<th>withscope. - Expecting
<col>to change text colour. It will not. Fix: use a class on the cells, or a CSS selector such astd:nth-child(2).
Practice Task: A Two-Term Report Card
Build a report card for one student with this structure:
- A caption with the student’s name and class
- A two-row header: “Subject” spans two rows, and “Term 1” and “Term 2” each span two columns
- Under each term, the columns “Theory” and “Practical”
- Three subject rows with
scope="row"headings - A
<tfoot>total row
<style>
table { border-collapse: collapse; }
th, td { border: 1px solid #555; padding: 6px 10px; text-align: center; }
</style>
<!-- TODO 1: Add a caption with the student's name and class -->
<!-- TODO 2: Header row 1: Subject (rowspan 2), Term 1 (colspan 2), Term 2 (colspan 2) -->
<!-- TODO 3: Header row 2: Theory, Practical, Theory, Practical -->
<!-- TODO 4: Three subject rows, with the subject as th scope="row" -->
<!-- TODO 5: A tfoot row with the totals of each column -->
<table>
</table>
Show one possible answer (try it yourself first!)
<style>
table { border-collapse: collapse; }
th, td { border: 1px solid #555; padding: 6px 10px; text-align: center; }
thead { background: #e8eaf6; }
tfoot { background: #fff8e1; font-weight: bold; }
</style>
<table>
<caption>Report Card: Sneha Iyer, Class 11 A</caption>
<thead>
<tr>
<th scope="col" rowspan="2">Subject</th>
<th scope="colgroup" colspan="2">Term 1</th>
<th scope="colgroup" colspan="2">Term 2</th>
</tr>
<tr>
<th scope="col">Theory</th><th scope="col">Practical</th>
<th scope="col">Theory</th><th scope="col">Practical</th>
</tr>
</thead>
<tbody>
<tr><th scope="row">Physics</th><td>58</td><td>27</td><td>62</td><td>28</td></tr>
<tr><th scope="row">Chemistry</th><td>61</td><td>26</td><td>65</td><td>29</td></tr>
<tr><th scope="row">Biology</th><td>66</td><td>28</td><td>63</td><td>27</td></tr>
</tbody>
<tfoot>
<tr><th scope="row">Total</th><td>185</td><td>81</td><td>190</td><td>84</td></tr>
</tfoot>
</table>
Key Takeaways
<caption>names the table, and<thead>,<tbody>and<tfoot>group its rows.scope="col"andscope="row"tell screen readers which cells a heading describes.colspanstretches a cell across columns androwspanstretches it down rows. Every row must still add up to the same width.<colgroup>and<col>style whole columns, but only a few properties such as background and width.- Keep tables simple, use them only for data, and wrap wide tables so they scroll on phones.