Day 7: HTML Tables & Semantic Data Presentation
We’ve structured text, connected pages, embedded images, and grouped items. Now let’s tackle structured data.
Tables carry a lot of historical baggage. In the 1990s and early 2000s, developers abused <table> tags to build entire page layouts. It created a generation of inaccessible, rigid websites that broke the moment screen sizes changed. Modern HTML has completely retired that pattern. Today, tables have one job: presenting structured, two-dimensional data.
When used correctly, tables are incredibly powerful. They organize complex information, print cleanly, scale gracefully with CSS, and give assistive technology a clear map of row and column relationships. But get the structure wrong, and you’ll build a wall of data that screen readers can’t parse and mobile users can’t navigate.
In this post, we’ll cover how to build semantic, accessible data tables from scratch. We’ll skip forms, multimedia, and CSS styling for now—those get their own posts next. Today is purely about table structure, meaning, and best practices.
🎯 What You’ll Learn Today
By the end of this post, you will:
- Understand the core table structure (
<table>,<tr>,<td>) - Use semantic grouping tags (
<thead>,<tbody>,<tfoot>) correctly - Apply
<th>withscopefor proper header association and screen reader support - Add
<caption>to describe table context programmatically - Build a clean, accessible data table from scratch
- Recognize and avoid common table anti-patterns
1. The Core Anatomy: <table>, <tr>, <td>
A table is built in a strict hierarchy:
<table>
<tr>
<td>Row 1, Cell 1</td>
<td>Row 1, Cell 2</td>
</tr>
<tr>
<td>Row 2, Cell 1</td>
<td>Row 2, Cell 2</td>
</tr>
</table>
<table>: The outer container. It tells the browser to render its contents in a grid.<tr>(table row): Wraps each horizontal line of cells. You can’t place content directly inside<table>; it must live inside a row.<td>(table data): Holds the actual content of a single cell.
Think of it like a spreadsheet. You define the sheet (<table>), then each line (<tr>), then each box on that line (<td>). Browsers handle the visual alignment automatically. Your job is to supply clean, hierarchical markup.
2. Semantic Grouping: <thead>, <tbody>, <tfoot>
Just listing <tr> elements works, but it misses the structural cues that make tables professional and accessible. Modern tables should be split into three logical zones:
<table>
<thead>
<tr><th>Header</th></tr> </thead>
<tbody>
<tr><td>Data</td></tr>
</tbody>
<tfoot>
<tr><td>Summary</td></tr>
</tfoot>
</table>
<thead>: Contains your column headers. Helps browsers repeat headers when printing multi-page tables, enables sticky headers with CSS later, and signals “this is the top row” to assistive tech.<tbody>: Holds the actual data rows. You can use multiple<tbody>blocks if you’re grouping related sections of data.<tfoot>: Reserved for summaries, totals, or footnotes. Historically rendered at the bottom, HTML5 allows it to appear anywhere inside<table>for parsing efficiency, but browsers still display it at the end.
Rule of thumb: Always include at least <thead> and <tbody> in any table you publish. It costs nothing semantically and pays off immediately in accessibility and styling flexibility.

3. Headers & Accessibility: <th> & scope
Inside <thead>, you should replace <td> with <th> (table header). Browsers will bold and center it by default, but the visual change isn’t why we use it. <th> carries semantic weight.
Screen readers need to know which headers apply to which data cells. That’s where the scope attribute comes in:
scope="col": This header applies to every cell below it in that column.scope="row": This header applies to every cell to the right of it in that row.
Here’s what a properly scoped table looks like:
<thead>
<tr>
<th scope="col">Plan</th>
<th scope="col">Price</th>
<th scope="col">Storage</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Free</th>
<td>$0/mo</td>
<td>5 GB</td>
</tr>
<tr>
<th scope="row">Pro</th>
<td>$12/mo</td>
<td>100 GB</td>
</tr>
</tbody>
Without scope, a screen reader just reads cell by cell: “Free, zero dollars, five gigabytes.” With it, the announcement becomes context-rich: “Plan, column header: Free. Price, column header: $0/mo.” It turns a data dump into a structured conversation.
4. Captions & Context: <caption>
Tables rarely make sense without a title. Instead of slapping an <h3> above the table, use the <caption> tag:
<table> <caption>Q3 2026 Subscription Plan Comparison</caption> <thead>...</thead> <tbody>...</tbody> </table>
<caption> must be placed immediately after the opening <table> tag. It provides a visible, semantic title that’s programmatically linked to the table in the accessibility tree. Screen readers announce it before reading the grid. Search engines use it to understand the table’s topic. Styling its position or font size is a CSS concern; HTML just establishes the relationship.
5. Beginner Pitfalls & Quick Fixes
Let’s address the traps that break tables in Week 2:
- Using tables for layout: Building sidebars, centering content, or faking responsive grids. → Fix: Tables are for data only. Use CSS Flexbox or Grid for page layout.
- Missing
<th>orscope: Leaves assistive technology guessing. → Fix: Always wrap headers in<th>and assign explicitscope. - Overcomplicating merges: Reaching for
colspanorrowspantoo early breaksscopelogic and makes mobile adaptation a nightmare. → Fix: Keep tables flat until you’re comfortable with responsive design patterns. We’ll cover cell merging later when you have the CSS foundation to handle it gracefully. - Debugging tip: Open DevTools → Elements tab. Expand your
<table>. If rows sit outside<thead>/<tbody>, or headers lackscope, structure needs cleanup. Validate quickly with the W3C Markup Validator to catch nesting errors instantly.
Your Turn: Build a Semantic Data Table
Let’s put structure into practice. Type this out by hand.
- Create a folder named
day-7-practiceand addindex.htmlwith the standard boilerplate. - Build a 3-column, 3-row comparison table (e.g., laptop specs: Model, RAM, Price).
- Wrap it in
<table>and add a<caption>describing what it compares. - Use
<thead>with<th scope="col">for headers. - Use
<tbody>for data rows. Add<th scope="row">to the first column. - Add a
<tfoot>with a summary row (e.g., “Best value: Pro Model”). - Open in browser, inspect in DevTools, and verify the hierarchy matches the semantic tags.
Stretch prompt: Temporarily add colspan="2" to one <td>. Notice how it breaks the column alignment and makes scope harder to track. Remove it and restore the flat structure. Why do modern developers default to flat tables until responsiveness is handled?
Key Takeaways
<table>is strictly for structured data, never for page layout or visual spacing- Always group rows with
<thead>,<tbody>, and<tfoot>for semantic clarity <th>+scopeis non-negotiable for accessibility and screen reader context<caption>programmatically links a title to the table, beating standalone headings- Keep tables flat and simple; merge cells only when you have responsive fallbacks ready
What’s Next
In the next post, we’ll tackle HTML Forms & User Input. You’ll learn how to build interactive forms with <form>, connect labels to inputs for accessibility, group related fields with <fieldset> and <legend>, and leverage built-in validation attributes without writing a single line of JavaScript.
Preview question: If you’re building a sign-up form, how should each input field be explicitly connected to its text label so screen readers know exactly what to announce? Jot down your guess. We’ll cover it next.
← Day 6: HTML Lists for Organising Data | Day 7 | Day 8: HTML Forms & User Input →





Leave a Reply