Responsive HTML Tables
Tables are wonderful on a big monitor. On a phone, they are a problem. A table with six columns needs room, and a phone screen is about 360 pixels wide. If you do nothing, one of three things happens: the table squeezes until the text wraps into thin columns of two letters each, it pushes the page wider than the screen so the whole page scrolls sideways, or it gets cut off and the right-hand data simply disappears.
Most visitors to your site arrive on a phone, so this matters. The good news is that there are several well-tested ways to handle it, from a one-line fix to a full redesign of the layout. This lesson explains each one, with its strengths and its trade-offs, so you can choose the right technique for your data.
What You Will Learnβ
- Why tables break on small screens
- How to set up the page correctly with the viewport meta tag
- How to make a table scroll sideways, accessibly
- How to keep the first column visible with sticky positioning
- How to make a table fluid
- How to stack a table into cards on small screens, and the risks
- How to prioritize columns
- How to choose between techniques
First, the Viewportβ
Responsive design starts with one line in your page's <head>. Without it, phones pretend to be a wide desktop screen and shrink your page.
<meta name="viewport" content="width=device-width, initial-scale=1" />
Most project templates (and Docusaurus) include this already. Check that yours does before you debug anything else.
Why Tables Are Hardβ
A table has a natural minimum width. Each column needs enough space for its longest unbreakable piece of content, like a word, a number, or a date. Add up all the columns, and the table needs a certain width, whatever the screen.
| Problem | What you see |
|---|---|
| Table wider than the screen | The whole page scrolls sideways, and the layout looks broken |
| Cells squeezed too narrow | Words broken into tiny pieces, tall, hard-to-read rows |
| Content cut off | Data missing, with no hint that there is more |
There is no single perfect answer. Instead, there is a toolbox.
Technique 1: A Scrolling Containerβ
The simplest and most reliable approach: keep the table as it is, and let it scroll sideways inside its own box. The page does not move. Only the table area does.
- HTML + CSS
- Browser Output
.table-wrap {
overflow-x: auto;
}
.table-wrap table {
min-width: 36rem; /* the table will not squeeze below this */
}
<div class="table-wrap">
<table>
...
</table>
</div>
| Course | Level | Weeks | Hours | Price (USD) |
|---|---|---|---|---|
| Intro to HTML | Beginner | 4 | 16 | 0 |
| CSS Layouts | Intermediate | 6 | 24 | 19 |
The demo box is limited to 340 pixels, like a phone. Drag sideways inside it, and the table scrolls while the surrounding page stays still.
| Pros | Cons |
|---|---|
| One small CSS rule | Users must scroll sideways |
| Keeps the real table structure, so accessibility stays intact | Not obvious that there is more to see |
| Works for any number of columns | Hard to compare columns that are far apart |
Make the Scroll Area Accessibleβ
A box that scrolls must be reachable without a mouse. Keyboard users cannot scroll a region that cannot receive focus.
<div
class="table-wrap"
role="region"
aria-labelledby="catalog-caption"
tabindex="0"
>
<table>
<caption id="catalog-caption">Course catalog</caption>
...
</table>
</div>
.table-wrap:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
tabindex="0"lets the keyboard reach the box, so the arrow keys can scroll it.role="region"andaria-labelledbygive the box a name, so a screen reader can announce it.- The focus outline shows keyboard users where they are.
If your table can scroll, use these three things together. You will see the same advice in Table Accessibility.
Hint That There Is Moreβ
People do not always realize a table scrolls. A simple hint helps, such as a note ("Scroll sideways to see more") or a fading edge made with CSS.
.table-wrap {
overflow-x: auto;
background:
linear-gradient(to right, #fff 30%, transparent) left / 2rem 100% no-repeat local,
linear-gradient(to left, #fff 30%, transparent) right / 2rem 100% no-repeat local,
linear-gradient(to right, rgba(0,0,0,.15), transparent) left / 0.75rem 100% no-repeat scroll,
linear-gradient(to left, rgba(0,0,0,.15), transparent) right / 0.75rem 100% no-repeat scroll;
}
This draws a soft shadow at an edge when more content waits beyond it. If the gradient code looks heavy, a simple written hint works just as well.
Technique 2: Keep the First Column Visibleβ
When a table scrolls sideways, the row labels in the first column scroll away, and users lose track of which row they are in. A sticky first column keeps it in place.
- HTML + CSS
- Browser Output
.table-wrap {
overflow-x: auto;
}
.table-wrap th:first-child,
.table-wrap td:first-child {
position: sticky;
left: 0;
background: #ffffff; /* solid, so scrolling content does not show through */
z-index: 1;
}
| Course | Level | Weeks | Hours | Price (USD) |
|---|---|---|---|---|
| Intro to HTML | Beginner | 4 | 16 | 0 |
| CSS Layouts | Intermediate | 6 | 24 | 19 |
Scroll the demo sideways: the course names stay put while the other columns slide past.
A wide sticky column leaves very little room for scrolling data on a phone. Keep it as short as you can.
Technique 3: Fluid Tablesβ
Sometimes a table can simply shrink and wrap, without scrolling. That works when:
- There are only a few columns (about two to four).
- The content is short text.
- You allow words to wrap.
table {
width: 100%;
table-layout: fixed; /* columns share the width equally (or per <col>) */
}
th,
td {
overflow-wrap: anywhere; /* break long words and URLs instead of overflowing */
padding: 0.5rem;
}
You can also shrink the text and padding a little on small screens:
@media (max-width: 40rem) {
th,
td {
padding: 0.4rem 0.5rem;
font-size: 0.875rem;
}
}
| Pros | Cons |
|---|---|
| No sideways scrolling | Cells get tall and narrow |
| Works well for small, simple tables | Breaks down quickly with many columns |
| Keeps semantics fully intact | Text can become hard to read if it is too small |
Use <col> widths (see Colgroup and Col) when you want some columns wider than others.
Technique 4: Stacked Rows (Cards)β
On narrow screens, you can turn each row into a small card, with the cell labels shown beside the values. No scrolling is needed, and everything reads top to bottom.
Wide screen Phone screen
ββββββββββ¬ββββββββ¬βββββββ ββββββββββββββββββββββ
β Course β Weeks β Levelβ β Course: Intro HTML β
ββββββββββΌββββββββΌβββββββ€ β Weeks: 4 β
β HTML β 4 β Beginβ β Level: Beginner β
β CSS β 6 β Interβ ββββββββββββββββββββββ€
ββββββββββ΄ββββββββ΄βββββββ β Course: CSS β
β Weeks: 6 β
β Level: Intermediateβ
ββββββββββββββββββββββ
The Codeβ
First, add a data-label to each data cell, repeating its column heading:
<table class="stack">
<caption>Course catalog</caption>
<thead>
<tr>
<th scope="col">Course</th>
<th scope="col">Weeks</th>
<th scope="col">Level</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" data-label="Course">Intro to HTML</th>
<td data-label="Weeks">4</td>
<td data-label="Level">Beginner</td>
</tr>
<tr>
<th scope="row" data-label="Course">CSS Layouts</th>
<td data-label="Weeks">6</td>
<td data-label="Level">Intermediate</td>
</tr>
</tbody>
</table>
Then use CSS on small screens to stack the cells, and to show the labels using attr():
@media (max-width: 40rem) {
.stack thead {
/* hide the header row visually, but keep it for screen readers */
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.stack tr {
display: block;
margin-bottom: 1rem;
border: 1px solid #cbd5e1;
border-radius: 8px;
}
.stack th,
.stack td {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.5rem 0.75rem;
border: 0;
border-bottom: 1px solid #e2e8f0;
}
.stack th::before,
.stack td::before {
content: attr(data-label);
font-weight: 600;
}
}
The Accessibility Warningβ
This technique is popular, and it is also the riskiest. When you change display on table elements, some browser and screen reader combinations stop treating the content as a table. The rows, columns, and headings that make it understandable can vanish. The labels added by ::before are generated content, which some screen readers do not read.
| Pros | Cons |
|---|---|
| No sideways scrolling | Can remove table semantics from assistive technology |
| Easy to read on a phone | Generated labels may not be read aloud |
| Looks modern | Hard to compare rows or columns |
Extra code (data-label on every cell) |
If you use it, take these precautions:
- Keep the header in the HTML, hidden visually but not removed, as shown above.
- Test with a screen reader on a real device.
- Add table roles back if the semantics are lost:
<table class="stack" role="table">
<thead role="rowgroup">
<tr role="row">
<th role="columnheader" scope="col">Course</th>
...
<tbody role="rowgroup">
<tr role="row">
<th role="rowheader" data-label="Course">Intro to HTML</th>
<td role="cell" data-label="Weeks">4</td>
If your data is for comparing (numbers across rows and columns), stacked cards often make the task harder for everyone. Scrolling inside a labeled region is usually the safer choice. Use stacking for simple lists of records, such as contacts or orders, where each row is read on its own.
Technique 5: Priority Columnsβ
On a small screen, not every column matters equally. You can show the important columns, and offer the others on demand.
@media (max-width: 40rem) {
.optional {
display: none;
}
}
<th scope="col" class="optional">Hours</th>
...
<td class="optional">16</td>
This hides the "Hours" column on narrow screens.
| Pros | Cons |
|---|---|
| Fits the table in the screen without scrolling | Hides data from people on small screens |
| Simple | Setting display: none removes the cells from screen readers too, creating different content for different devices |
Hiding columns means some visitors never see the information. If you use this technique, provide another way to reach it: a "Show all columns" button, a link to a full-width view, or a download of the data. And never hide a column that is essential to understanding a row.
Technique 6: Rethink the Dataβ
Sometimes the best answer is not a table at all on small screens. Think about what your reader needs.
| If the content is... | Consider... |
|---|---|
| A list of records you read one by one (orders, contacts) | A list of cards, built with real list markup |
| A comparison between two or three items | Two or three short sections, each with a description list |
| A big dataset to explore | A scrolling table in a labeled region, plus a download as CSV link |
| A pricing grid | A transposed table (plans as rows) or stacked plan cards |
For one item's properties, a description list is often cleaner than a table with a single row.
Choosing a Techniqueβ
| Situation | Best choice |
|---|---|
| Data table with many columns, users compare values | Scrolling container with a sticky first column |
| Small table, short text, 2 to 4 columns | Fluid table that wraps |
| Simple records that are read row by row | Stacked cards, with care |
| A few columns are optional | Priority columns, with a way to see all |
| Table is really a list of records | A list or description lists |
A good default: wrap the table in an accessible scrolling container. It is simple, keeps the table intact, and works for nearly every table.
Putting It Together: A Safe Starterβ
This version combines the safest ideas: a scrolling region, a sticky first column, comfortable spacing, and a minimum width that stops the table from squeezing.
<div class="table-wrap" role="region" aria-labelledby="plans-caption" tabindex="0">
<table class="data-table">
<caption id="plans-caption">Plans and prices (USD per month)</caption>
<thead>
<tr>
<th scope="col">Plan</th>
<th scope="col">Projects</th>
<th scope="col">Storage</th>
<th scope="col">Support</th>
<th scope="col">Price</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Free</th>
<td>3</td><td>1 GB</td><td>Community</td><td>0</td>
</tr>
<tr>
<th scope="row">Pro</th>
<td>Unlimited</td><td>50 GB</td><td>Email</td><td>9</td>
</tr>
</tbody>
</table>
</div>
.table-wrap {
overflow-x: auto;
}
.table-wrap:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
.data-table {
border-collapse: collapse;
width: 100%;
min-width: 36rem;
}
.data-table th,
.data-table td {
padding: 0.6rem 0.9rem;
border-bottom: 1px solid #e2e8f0;
text-align: start;
white-space: nowrap;
}
.data-table tbody th[scope="row"] {
position: sticky;
left: 0;
background: #ffffff;
}
Test on Real Screensβ
- Resize your browser window down to 320 pixels wide, the narrowest common phone.
- Use the device toolbar in your browser's developer tools to simulate phones.
- Test on a real phone if you can. Scrolling and touch feel different from a mouse.
- Zoom to 200%. Tables should still be usable.
- Try the keyboard. Can you focus and scroll the region?
- Listen with a screen reader, especially if you restyled table
displayvalues. - Turn the phone sideways. Landscape mode gives you extra width, and your table should handle both.
Global Audience Notesβ
- Many visitors use small, older phones on slow connections. Simple techniques (scrolling, fluid) are lighter than heavy JavaScript solutions.
- Translations can be much longer. A column that fits "Level" in English may need far more room in other languages. Allow wrapping, and avoid fixed pixel widths.
- Right-to-left pages scroll from the right, and sticky columns should use
inset-inline-start(orright: 0) so the label column stays at the correct edge. - Offer data downloads, such as CSV, for big tables, for people who prefer to work offline or in a spreadsheet.
Common Mistakes to Avoidβ
- Forgetting the viewport meta tag.
- Letting a wide table push the whole page sideways instead of scrolling inside a container.
- A scrolling container that is not keyboard-accessible. Add
tabindex="0", a role, and a name. - Sticky cells with no background, so scrolling content shows through.
- Using
display: blockon table elements and never testing with a screen reader. - Hiding columns without any way to reach their data.
- Fixed pixel widths on columns that overflow small screens.
- Tiny text to squeeze everything in. Keep it readable.
- Stacking comparison data so users cannot compare anything.
- Testing only on a desktop browser.
Practice Timeβ
Try these in your editor:
- Make a table with six columns, and view it at phone width to see the problem.
- Wrap it in a
divwithoverflow-x: auto. - Add
tabindex="0",role="region", andaria-labelledbyto the wrapper. - Add a visible
:focus-visibleoutline to the wrapper. - Make the first column sticky, with a solid background.
- Set a
min-widthon the table so it does not squeeze too far. - Try
table-layout: fixedwithoverflow-wrap: anywhereon a small three-column table. - Build the stacked card layout with
data-labelfor a simple contact list. - Add a media query that hides one optional column, and add a "show all" link.
- Bonus: test each version with the keyboard, then with a screen reader. Which one was easiest to use?
Quick Recapβ
Click a question to check your answer.
1. What is the simplest way to make a wide table work on a phone?
Wrap it in a container with overflow-x: auto, so the table scrolls sideways inside its own box while the page stays still.
2. How do you make a scrolling table container accessible?
Give it tabindex="0" so the keyboard can focus it, role="region" with aria-labelledby (pointing to the caption) so it has a name, and a visible focus outline.
3. How do you keep the first column visible while scrolling?
Use position: sticky; left: 0; on the first-column cells, with a solid background so other content does not show through.
4. What is the main risk of stacked card layouts?
Changing the display of table elements can remove table semantics from assistive technology, and generated labels may not be read aloud. Test with a screen reader, keep the header available, and consider adding table roles.
5. Why be careful with hiding columns on small screens?
Visitors on small screens would never see that data, and display: none also hides it from screen readers. Offer another way to reach it.
6. What is a good default technique for most data tables?
An accessible scrolling container: it keeps the real table structure, works with any number of columns, and needs only a little CSS.
Key Takeawaysβ
- Tables have a natural minimum width, so on small screens they need help. Start with the viewport meta tag.
- The safest default is a scrolling container with
overflow-x: auto, plustabindex="0",role="region", and a name. - Sticky first columns keep row labels visible while scrolling.
- Fluid tables work for small, simple tables. Stacked cards suit simple records, but need careful testing.
- Do not hide data without a path back, and do not break table semantics with
displaychanges. - Test on real phones, with a keyboard, and with a screen reader, and write for small, slow screens and long translations.
You have now covered every part of tables: structure, headings, captions, sections, merged cells, columns, styling, accessibility, and small screens. Return to the Tables overview to review the full path, or move on to the next section of the HTML course.