HTML tables have a reputation problem. Two decades ago developers used them for page layout, which broke accessibility and turned maintenance into a nightmare. The backlash pushed many engineers to avoid even for the one job it does well: rows and columns of related data.
A real table is the right element when the cell at row 4, column 2 means something. Product comparisons, pricing plans, schedules, KPI dashboards, sport standings. A
grid does not give screen readers any of that semantic information.The hard part is mobile. A six-column report that fits a 1440px monitor becomes a horizontal scroll on a 375px phone. That is a CSS problem, sometimes a layout problem, and occasionally a sign you should not be using a table at all.
Semantic Table Markup
A correct table uses every group element:
`html
| Product | Q1 | Q2 | Q3 |
|---|---|---|---|
| Widget A | 1,200 | 1,450 | 1,600 |
| Widget B | 800 | 920 | 1,100 |
| Total | 2,000 | 2,370 | 2,700 |
`: one-line description. Screen readers announce it before reading any rows.
, , : row groups. Browsers can scroll the body while keeping the header in place; screen readers use the grouping to navigate.
with scope: marks header cells with scope="col" or scope="row". Skip this and screen reader users have no way to know which header belongs to which cell.
If your data lives in a spreadsheet, dump it to CSV and convert with the JSON to CSV tool (it converts both directions). When the HTML is ready for production, run it through the HTML Minifier to drop whitespace.

Three Patterns That Survive Mobile
Pick the pattern that matches the data.
1. Horizontal scroll (simplest, no markup change):
`css
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
`
Wraps the table in a scrollable container. The structure stays intact. Good when every column carries equal weight.
2. Stacked rows (most readable on phones):
`css
@media (max-width: 768px) {
table, thead, tbody, th, td, tr { display: block; }
thead { display: none; }
td {
position: relative;
padding-left: 50%;
}
td::before {
content: attr(data-label);
position: absolute;
left: 0;
width: 45%;
font-weight: bold;
}
}
`
Each row turns into a card with labeled values. Requires data-label attributes on every . Best for 4 to 8 column tables where every value matters.
3. Column hiding (selective):
`css
@media (max-width: 768px) {
.priority-low { display: none; }
}
`
Hide secondary columns on mobile and add a "show all" toggle. Best for tables with a clear primary view and optional detail columns.
For very wide tables, combine: hide low-priority columns, then scroll the remainder.
Pick the pattern that matches the data.
CSS That Makes a Table Readable
Default tables look like a 1995 spreadsheet. A small ruleset fixes that:
`css
table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #e5e7eb; }
thead th { background-color: #f9fafb; font-weight: 600; color: #374151; text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; }
tbody tr:hover { background-color: #f3f4f6; }
tbody tr:nth-child(even) { background-color: #f9fafb; }
`
The rules that earn their keep:
border-collapse: collapseremoves the double border between cells. Set it on every table.- Zebra striping (
:nth-child(even)) helps eyes track a row across many columns. - Hover highlighting tells the user which row is under the cursor.
- Header styling (uppercase, smaller, tinted background) separates the header from the data.
- Alignment matters: left-align text, right-align numbers so digit places line up, center checkmarks and status icons.
When the stylesheet ships, run it through the CSS Minifier to drop comments and whitespace.
Accessibility Checklist
Tables are one of the trickiest elements for screen reader users. Run through this every time:
- Always include
: a one-line description of the table. Hide it visually with CSS if you must, but never drop it from the markup. withscopeon every header: column headers getscope="col", row headers getscope="row". Without scope, the row/column relationship is lost.- Do not use tables for layout: if there is no row-column meaning, use CSS Grid or Flexbox. Layout tables make screen readers announce nonsense.
- Avoid
colspanandrowspan: merged cells confuse keyboard navigation. When merging is unavoidable, useidandheadersto keep the relationship explicit. - Sortable headers: set
aria-sort="ascending","descending", or"none"on the active column header. - Stacked layout: keep
data-labeltext identical to the column header text so screen readers do not announce two different names for the same value. - Color is never the only signal: pair red/green with a
+or-sign, or with an icon, so color-blind users get the same information.

When Not to Use a Table
Some grids belong in a different element.
- Cards: if every row is a self-contained item (product, person, project) with images and actions, a card grid reads better than a table, especially on mobile.
- Definition lists: two-column "label / value" data fits a
and- better than a table, and is easier to style responsively.
with - Charts: if the question is "what is the trend?", a line or bar chart answers it faster than rows of numbers.
- Interactive data grids: filtering, grouping, pagination, virtualization, and cell editing all belong in a real grid component (AG Grid, TanStack Table). Trying to roll those on top of a styled
ends in pain.
The test: does the cell at row X, column Y mean something specific? If yes, table. If you only have a list of items with properties, cards or a
will do better.FAQ
Should I use a CSS framework for tables?
Tailwind and Bootstrap both ship table utilities that save time (Tailwind's divide-y and text-right are the two you will reach for most). For complex interactive tables, use a JS library like TanStack Table or AG Grid. For plain display tables, vanilla CSS is enough.
How do I make a table sortable?
For a static page, add click handlers on the elements that re-order the rows. For React or Vue, use TanStack Table for headless sort, filter, and pagination logic. For server-rendered pages, sort on the server and put the direction in a query parameter.
How many columns work on mobile?
Three or four columns fit a 375px screen without scrolling. Five to six need the stacked layout or horizontal scroll. Beyond six, switch to column hiding, expandable rows, or a card view.
Can I make table headers sticky?
Yes. position: sticky; top: 0; on the th elements keeps the header pinned while the body scrolls. Add a z-index and a solid background so cells do not bleed through.
### Should I use a CSS framework for tables.
Markdown Table Generator: Build Clean Tables Without the Pain
Markdown tables are simple until the pipes and dashes stop lining up. Learn the syntax, alignment tricks, and a free tool that formats tables for you.
CSV to JSON: Convert Spreadsheet Data for APIs and Code
Turn a CSV export into clean JSON for APIs, imports, and scripts. Learn how the conversion works, common pitfalls with types and quotes, and a free tool.
JSON Guide: Format, Validate, and Convert JSON Files
JSON guide for developers: syntax rules, common parse errors, formatting and schema validation, plus how to convert between JSON and CSV files.
