HTML Tables

HTML Tables Overview

HTML tables have a bit of a complicated reputation and I think it's worth addressing that upfront: for most of the early web, tables were used to build entire page layouts - two-column designs, sidebars, navigation, all of it crammed into nested table cells - and that was a genuinely terrible practice that made pages inaccessible, hard to maintain, and impossible to style properly. So when people say 'don't use tables', that's what they mean. The actual correct use of tables - displaying data that has real row-and-column relationships like a spreadsheet does - is completely valid and tables are the right tool for it, semantically and practically. A price comparison, a class schedule, a dataset with headers and values - that's what tables are for and there's no better HTML element for that job.

Key Benefits

  • Structure - Provides a genuine grid structure for two-dimensional data where rows and columns both carry meaning.
  • Semantics - The table element and its children communicate to browsers, screen readers, and search engines that the data has relational structure.
  • Accessibility - Properly marked up tables with header cells and scope attributes give screen reader users a way to navigate and understand complex data.

Core Components

A basic HTML table uses four tags and they all work together - the table as the outer container, tr for each row, th for header cells, and td for data cells - and once you see the pattern it's fairly intuitive, though writing tables by hand gets tedious fast for anything beyond a few rows.

html
1<table>
2  <tr>
3    <th>Product</th>
4    <th>Price</th>
5    <th>In Stock</th>
6  </tr>
7  <tr>
8    <td>Widget A</td>
9    <td>$12.99</td>
10    <td>Yes</td>
11  </tr>
12  <tr>
13    <td>Widget B</td>
14    <td>$24.99</td>
15    <td>No</td>
16  </tr>
17</table>

Essential Elements

  • <table> - The outer container for the entire table. Everything else lives inside this.
  • <tr> - Defines a row. Your table is essentially a stack of tr elements, each holding the cells for that row.
  • <th> - A header cell - bold and centered by default. Use these for column headers and row headers, not td, because th has semantic meaning that screen readers announce differently.
  • <td> - A standard data cell. This is where your actual data goes.

Semantic Table Structure

The basic table works but for anything more than a few rows you really want to add thead, tbody, and tfoot to divide the table into sections - and this isn't just for semantics, it has practical benefits too like allowing the tbody to scroll independently while the header stays visible, and giving you clean CSS hooks to style the header and footer rows differently from the data rows. I skipped these for a long time because they felt like extra tags for no reason and then I tried to print a long table and the headers only appeared on the first page, which is a problem that thead fixes automatically in most browsers.

html
1<table>
2  <thead>
3    <tr>
4      <th>Month</th>
5      <th>Sales</th>
6      <th>Expenses</th>
7    </tr>
8  </thead>
9  <tbody>
10    <tr>
11      <td>January</td>
12      <td>$5,000</td>
13      <td>$3,200</td>
14    </tr>
15    <tr>
16      <td>February</td>
17      <td>$5,800</td>
18      <td>$3,500</td>
19    </tr>
20  </tbody>
21  <tfoot>
22    <tr>
23      <td>Total</td>
24      <td>$10,800</td>
25      <td>$6,700</td>
26    </tr>
27  </tfoot>
28</table>

Semantic Elements

  • <thead> - Groups the header row or rows. In printed tables, browsers repeat these rows at the top of each page automatically.
  • <tbody> - Groups the main data rows. If you only use one of the three sectioning elements, make it this one.
  • <tfoot> - Groups footer rows like totals or summaries. Visually it renders at the bottom even if you write it before tbody in the HTML, which is a slightly odd quirk.

Advanced Table Features

Three features come up enough in real table work that they're worth knowing: the caption element for giving the table a title, colspan and rowspan for merging cells across columns or rows, and colgroup for applying styles to entire columns at once. Colspan and rowspan are the ones that trip people up most - I had to draw a grid on paper the first few times I used them to keep track of which cells were merging where - but they make sense once you think of the numbers as "this cell takes up X columns" rather than some kind of offset.

html
1<!-- Table with caption -->
2<table>
3  <caption>Monthly Sales Report 2024</caption>
4  <!-- table content -->
5</table>
6
7<!-- Column groups for styling whole columns -->
8<table>
9  <colgroup>
10    <col span="2" style="background-color: #f2f2f2;">
11    <col style="background-color: #e6f7ff;">
12  </colgroup>
13  <!-- table content -->
14</table>
15
16<!-- Cells spanning multiple columns -->
17<table>
18  <tr>
19    <th colspan="2">Name</th>
20    <th>Age</th>
21  </tr>
22  <tr>
23    <td>John</td>
24    <td>Doe</td>
25    <td>30</td>
26  </tr>
27</table>

Advanced Features

  • <caption> - Provides a visible title for the table that's also read by screen readers as the table's label. Goes directly after the opening table tag.
  • colspan and rowspan - colspan makes a cell span multiple columns horizontally, rowspan makes it span multiple rows vertically. The number is how many cells it covers.
  • <colgroup> and <col> - Lets you apply CSS to entire columns without adding a class to every single td in that column - genuinely useful for striping columns or highlighting one column.

Creating Accessible Tables

A basic table with th elements is already more accessible than no headers, but for tables where it's not immediately obvious which header governs which cell - multi-level headers, merged cells, tables where both rows and columns have headers - you need the scope attribute to tell screen readers explicitly how to associate headers with data. The scope attribute on a th can be set to col, row, colgroup, or rowgroup, and it gives a screen reader user navigating cell by cell the context of what header applies to what they're hearing. For very complex tables the headers attribute on td elements gives you even more explicit control, associating each data cell directly with specific header cell IDs.

html
1<!-- scope attribute on column headers -->
2<table>
3  <tr>
4    <th scope="col">Product</th>
5    <th scope="col">Price</th>
6    <th scope="col">In Stock</th>
7  </tr>
8  <tr>
9    <th scope="row">Widget A</th>
10    <td>$12.99</td>
11    <td>Yes</td>
12  </tr>
13</table>
14
15<!-- headers attribute for complex tables -->
16<table>
17  <tr>
18    <th id="product">Product</th>
19    <th id="price">Price</th>
20  </tr>
21  <tr>
22    <td headers="product">Widget A</td>
23    <td headers="price">$12.99</td>
24  </tr>
25</table>

Accessibility Features

  • scope - Added to th elements to specify whether the header applies to a column (col), row (row), or a group of columns or rows. Simple tables should always have this.
  • headers - Added to td elements in complex tables, referencing the IDs of the th elements that head that cell. More work to write but gives screen readers precise header associations.

Styling Tables with CSS

Default unstyled tables look pretty rough - no borders, inconsistent spacing, no visual separation between rows - so almost every real project adds at least some CSS to make them readable. The border-collapse: collapse declaration is the first thing most people add because without it you get double borders between cells which looks strange and the fix is one line. Zebra striping with tr:nth-child(even) is another very common pattern because alternating row colors make it much easier to track which row you're reading across a wide table, and I find it's one of those small things that makes a data-heavy page feel significantly more polished.

css
1table {
2  width: 100%;
3  border-collapse: collapse;
4  margin: 1em 0;
5  font-family: sans-serif;
6}
7th, td {
8  border: 1px solid #ddd;
9  padding: 0.75em;
10  text-align: left;
11}
12th {
13  background-color: #f2f2f2;
14  font-weight: bold;
15}
16
17/* Zebra striping */
18tr:nth-child(even) {
19  background-color: #f9f9f9;
20}
21
22/* Hover highlight */
23tr:hover {
24  background-color: #f1f1f1;
25}
26
27/* Responsive wrapper */
28.table-container {
29  overflow-x: auto;
30  max-width: 100%;
31}
32@media screen and (max-width: 600px) {
33  table {
34    font-size: 0.8em;
35  }
36  th, td {
37    padding: 0.5em;
38  }
39}

Styling Techniques

  • border-collapse: collapse - The single most important table CSS declaration - merges the borders between cells so you get one clean line instead of two borders with a gap between them.
  • Zebra striping - Alternating row background colors using tr:nth-child(even) - makes wide tables much easier to read across a row without losing your place.
  • Responsive wrapper - Wrapping the table in a div with overflow-x: auto is the simplest way to handle tables on small screens - the table keeps its structure and the user can scroll horizontally.

Best Practices

The short version of best practices for HTML tables is: use them only when the data genuinely has row-and-column relationships, always use th for header cells with scope attributes, add thead and tbody to divide the structure, wrap wide tables in a scrollable container for mobile, and include a caption if the table's purpose isn't immediately obvious from the surrounding content. The one rule people break most often is using td instead of th for header cells - it looks the same after you add CSS but it breaks accessibility because screen readers announce th cells differently and use them for navigation.

html
1<!-- Good: genuine tabular data with proper structure -->
2<table>
3  <caption>Q1 Product Sales</caption>
4  <thead>
5    <tr>
6      <th scope="col">Product</th>
7      <th scope="col">Units Sold</th>
8      <th scope="col">Revenue</th>
9    </tr>
10  </thead>
11  <tbody>
12    <tr>
13      <th scope="row">Widget A</th>
14      <td>142</td>
15      <td>$1,843</td>
16    </tr>
17  </tbody>
18</table>
19
20<!-- Wrong: using a table for two-column page layout -->
21<!-- Use CSS Flexbox or Grid for layout instead -->

Common Mistakes

Using tables for layout is the big one and has been covered, but there are a few other mistakes that show up regularly. Using td instead of th for header cells is probably the most common technical error - the visual difference with CSS is nothing but semantically they're very different and your table will be harder to navigate with a screen reader. Not adding any headers at all is even worse. The other thing worth mentioning is that old HTML attributes like cellpadding, cellspacing, and border directly on the table tag are deprecated in HTML5 - they still work in most browsers but they're not valid HTML and all that styling should be in your CSS.

html
1<!-- Wrong: table used for layout -->
2<table>
3  <tr>
4    <td>Sidebar content</td>
5    <td>Main content</td>
6  </tr>
7</table>
8
9<!-- Right: CSS handles layout -->
10<div class="layout">
11  <aside>Sidebar content</aside>
12  <main>Main content</main>
13</div>
14
15<!-- Wrong: td used for column headers -->
16<table>
17  <tr>
18    <td>Product</td>
19    <td>Price</td>
20  </tr>
21</table>
22
23<!-- Right: th with scope for headers -->
24<table>
25  <tr>
26    <th scope="col">Product</th>
27    <th scope="col">Price</th>
28  </tr>
29</table>

When to Use Tables

The test for whether something should be a table is whether the data has meaningful relationships in two directions - across rows and down columns - where both directions carry information. A price comparison where each row is a product and each column is a feature, and the intersection of any row and column tells you something specific: that's a table. A list of team members with their job titles: that could go either way, but if you're just showing name and role with no complex cross-referencing it might be cleaner as a definition list or a styled div structure. Navigation menus, card grids, article layouts - those are all CSS layout jobs regardless of how they look visually.

html
1<!-- Use table for: financial reports, product comparisons,
2     schedules, statistical datasets, anything where both
3     rows AND columns carry meaning -->
4
5<!-- Use CSS Flexbox or Grid for:
6     page structure, card layouts, navigation,
7     anything that's layout rather than data -->
8
9<!-- Use lists for:
10     simple item collections, navigation links,
11     term-definition pairs (use dl) -->

Tables: The Right Tool for the Right Data

Tables got a bad reputation from years of being misused for layout and I think that reputation has overcorrected to where some developers avoid them even for genuinely tabular data, which is a mistake in the other direction. For data that has real row-and-column structure, the table element with its thead, tbody, th, and scope attributes gives you semantics, accessibility, and styling hooks that no other HTML structure provides. The key is the scope question: if you removed either the row labels or the column labels and the data still made complete sense, it's probably not a table. If both directions of labeling are needed to understand what any given cell means, it almost certainly is

Frequently Asked Questions

Are tables bad for SEO?

Tables used for actual tabular data are not bad for SEO - search engines can read table structure and sometimes pull table data into featured snippets and knowledge panels, which can increase visibility. What was historically bad for SEO was using tables for page layout, because it created deeply nested markup with poor semantic structure that was harder for crawlers to parse. For data tables, using them correctly is fine.

How do I make complex tables accessible?

For simple tables, adding scope attributes to your th elements is usually enough - scope="col" on column headers and scope="row" on row headers. For tables with merged cells or multiple header levels, you need to add id attributes to each th and then reference those IDs using the headers attribute on the relevant td elements. It's more work to write but it gives screen reader users precise navigation. Also add a caption element if the table's purpose isn't obvious from context.

Can I put other HTML elements inside table cells?

Yes, td and th can contain most HTML elements including paragraphs, images, lists, links, and forms. Nested tables technically work but are worth avoiding unless you genuinely need them - they create complex markup that's hard to maintain and read, and most of the time what looks like it needs nested tables can be solved with a better table structure or a different element altogether.

How do I center a table on the page?

Set margin-left and margin-right to auto on the table element in CSS - but the table also needs a width less than 100% for margin auto to have any effect, since a full-width table is already centered by definition. So: table { width: 80%; margin-left: auto; margin-right: auto; } or whatever width suits your design.

What is the difference between cellpadding and cellspacing?

These are old HTML attributes that are deprecated in HTML5 and shouldn't be used in new code. Cellpadding controlled the space between a cell's content and its border - that's now CSS padding on td and th. Cellspacing controlled the gap between cells - that's now CSS border-spacing on the table element, though most modern tables use border-collapse: collapse which eliminates the gap entirely.

How do I make a table responsive on mobile?

The simplest approach is to wrap the table in a div with overflow-x: auto - this lets the table keep its full structure while allowing the user to scroll horizontally on small screens, which is more accessible than trying to reformat the table. For very wide tables on mobile you can also use CSS to switch the display mode entirely - stacking cells vertically using display: block on tr and td with data-label attributes to show the column name - but that's a more involved technique and only worth the complexity for tables with lots of columns.