Tables and figures
Tables for tabular data, figures for illustrations. Two structures, two jobs, frequently confused.
Tables and figures are the two structural elements newcomers misuse the hardest — tables because they used to be the layout system, figures because they look like decoration. They aren't either. A table is a grid of data with rows and columns that mean something to each other; a figure is an illustration with a caption that ties it to the prose.
When tables are the right tool
The honest test: would the data still make sense if you swapped two rows or two columns? If yes, it isn't a table — it's a list of items that happen to render in columns. Reach for <ul> plus CSS grid instead.
A real table has headers that label rows or columns, and a screen-reader user can navigate cell-to-cell and ask "what column is this in?" That's the model: rows × columns × headers, with the headers giving every cell its meaning.
Genuine table cases:
- A pricing table comparing plans (rows: features; columns: plans).
- A schedule (rows: time slots; columns: rooms or tracks).
- A spreadsheet-shaped readout (rows: items; columns: properties).
Layout cases — a sidebar next to a main column, three feature blocks next to each other — are not tables. CSS Grid and Flexbox are the answer. Using <table> for layout pollutes the accessibility tree: screen readers announce "table with 3 rows and 2 columns" when the user just wanted to read your homepage.
Table anatomy
A complete table has more parts than most beginners use:
<table>— the root.<caption>— the table's title, sitting above (or below, with CSS) the data. Screen readers announce it as the table's name.<thead>,<tbody>,<tfoot>— group the header, body, and footer rows. Browsers use them for printing (repeating thead on each page) and for sticky headers.<tr>— table row.<th>— header cell. Usescope="col"for column headers,scope="row"for row headers. The scope tells screen readers which cells this header labels.<td>— data cell.
<table>
<caption>Conference schedule, day 1</caption>
<thead>
<tr>
<th scope="col">Time</th>
<th scope="col">Track A</th>
<th scope="col">Track B</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">09:00</th>
<td>Opening keynote</td>
<td>Open</td>
</tr>
<tr>
<th scope="row">10:00</th>
<td>HTML for working programmers</td>
<td>CSS layout in 2025</td>
</tr>
</tbody>
</table>When the table has time-row headers and topic-column headers, both axes need <th> with the right scope. A screen reader navigating to "HTML for working programmers" can then announce "10:00, Track A" — the two headers that give that cell its meaning.
colspan and rowspan let a single cell cover multiple columns or rows. Useful for hierarchical headers ("Q1 2025" spanning Jan/Feb/Mar columns underneath). Use them only when the data really does span — don't fake layout this way.
Figures
A <figure> wraps content that could be referenced from elsewhere in the document — an illustration, a code block, a chart, a quote — together with its <figcaption>. It's the markup equivalent of a printed book's "Figure 3.2: …"
<figure>
<img src="parser-flow.svg"
alt="Diagram of the HTML parser, with input bytes flowing into a tokenizer, then a tree builder, then the DOM.">
<figcaption>The HTML parser, in three stages.</figcaption>
</figure>The caption isn't a tooltip and isn't a duplicate of the alt. Alt is what a screen reader reads when it encounters the image; the caption is part of the visible prose, anchoring the figure to the surrounding paragraphs. They serve different audiences and should usually say different things.
<figure> is a sectioning root — a <h1> inside it doesn't bleed into the document outline. Practically, that means a figure can carry its own little micro-hierarchy without disturbing the page's structure.
The figure isn't only for images. Code listings, blockquotes, charts (a <canvas> or inline SVG), even a video can sit inside one when the surrounding prose refers to "the example below" or "Figure 4."
<figcaption>?