Responsive Tables on Small Screens
A table with eight columns cannot fit on a phone. Four patterns that keep the data usable, and the trade off each one makes.
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
Concept
Tables are the hardest thing on the web to make responsive, because a table is a grid and a phone screen is a column. Every solution is a compromise; the skill is choosing which compromise fits the data.
Pattern 1: horizontal scroll
The simplest and the safest. The table keeps its structure and the reader scrolls sideways.
<div class="table-scroll" tabindex="0" role="region" aria-labelledby="fees-caption">
<table>
<caption id="fees-caption">Fee structure by course and year</caption>
...
</table>
</div>.table-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-scroll table {
min-width: 40rem; /* below this the columns are unreadable anyway */
border-collapse: collapse;
}Three details make it work properly. tabindex="0" lets a keyboard user reach and scroll the container. role="region" with a label means a screen reader announces what is being scrolled. And a shadow at the edge tells the reader there is more:
.table-scroll {
background:
linear-gradient(to right, #fff 30%, rgba(255,255,255,0)),
linear-gradient(to left, #fff 30%, rgba(255,255,255,0)) 100% 0,
radial-gradient(farthest-side at 0 50%, rgba(15,23,42,.15), transparent),
radial-gradient(farthest-side at 100% 50%, rgba(15,23,42,.15), transparent) 100% 0;
background-repeat: no-repeat;
background-size: 40px 100%, 40px 100%, 14px 100%, 14px 100%;
background-attachment: local, local, scroll, scroll;
}Trade off: none structurally. The reader has to scroll, which is honest.
Pattern 2: stacked cards
Below a breakpoint each row becomes a block, with the column name printed beside each value using a data attribute.
<tbody>
<tr>
<th scope="row" data-label="Route">12A</th>
<td data-label="From">Station Road</td>
<td data-label="First bus">06:10</td>
<td data-label="Frequency">Every 20 minutes</td>
</tr>
</tbody>@media (max-width: 40rem) {
table, thead, tbody, tr, th, td { display: block; }
thead { position: absolute; left: -10000px; } /* off screen, still announced */
tr {
border: 1px solid #cbd5e1;
border-radius: 8px;
margin-bottom: 1rem;
padding: 0.5rem;
}
td, th {
display: grid;
grid-template-columns: 9rem 1fr;
gap: 0.75rem;
padding: 0.35rem 0;
text-align: left;
}
td::before, th::before {
content: attr(data-label);
font-weight: 600;
color: #475569;
}
}Trade off: a real one. Setting display: block on table elements strips their table semantics in most browsers, so a screen reader no longer announces headers with cells. The data-label text is generated content and is announced inconsistently. Use this pattern for short, simple tables where losing the grid semantics does not lose the meaning - and duplicate the label in real text if the data is important.
Pattern 3: hide low priority columns
<th scope="col" class="col-optional">Room</th>
...
<td class="col-optional">B12</td>@media (max-width: 40rem) {
.col-optional { display: none; }
}Straightforward and often right - a timetable on a phone rarely needs the room number in the same glance. Pair it with a link to a full view.
Trade off: data disappears. Only hide what is genuinely secondary, and never hide it silently on a page where the reader is comparing values.
Pattern 4: pivot the table
Swap the axes so each record becomes a column. Useful for comparison tables with few records and many attributes - three plans compared across twelve features, for instance.
Trade off: only works with a small number of records, and usually means maintaining two markup variants.
Choosing
| Situation | Pattern |
|---|---|
| Many columns, data must stay comparable | Horizontal scroll |
| Few columns, each row read on its own | Stacked cards |
| Some columns clearly secondary | Hide columns |
| Few records, many attributes | Pivot |
| Long numeric table for analysis | Scroll, plus a download link |
What helps in every pattern
- A sticky first column, so the row label stays visible while scrolling sideways.
font-variant-numeric: tabular-numsso digits align.- Right aligned numbers, left aligned text.
- Generous cell padding; a cramped table is unreadable at any width.
- A visible caption that survives every breakpoint.
tbody th[scope="row"] {
position: sticky;
left: 0;
background: #ffffff;
}
td.numeric {
text-align: right;
font-variant-numeric: tabular-nums;
}Important rules
- Changing
displayon table elements removes table semantics. That is the cost of the stacked pattern. - A scroll container must be keyboard reachable and labelled.
- Never rely on generated content alone to carry data.
- Set
min-widthon a scrolling table, or it will squeeze itself into unreadable columns instead of scrolling. - Test at three hundred and twenty pixels wide, the narrowest screen still in common use.
Common mistakes
- Letting a table overflow the page so the whole document scrolls sideways.
- Using the stacked pattern on a large data table and destroying its semantics.
- Hiding columns that the reader actually needs.
- A scroll container with no keyboard access.
- Shrinking font size until the table fits, producing text nobody can read.
- Testing only in a resized desktop window rather than on a real device.
Best practices
- Start with horizontal scroll. It is the safest default and needs no compromise.
- Reach for stacked cards only when the table is small and simple.
- Add a sticky row header for wide tables.
- Offer a CSV or PDF download for large datasets.
- Ask whether the data needs to be a table on a phone at all - a chart or a summary may serve better.
Practice
- Wrap a wide table in a labelled, focusable scroll container and confirm you can scroll it with the keyboard alone.
- Build the stacked card pattern for a four column table, then test it with a screen reader and describe what is lost.
- Make the first column sticky and scroll a ten column table sideways.
- Test a table at three hundred and twenty pixels and list every problem you find.