Why Tables Break On Small Screens
Tables present data in a grid where meaning comes from the relationship between a cell and its row and column headers. Scanning down a column allows comparison; scanning across a row reveals the full record for one item. That model depends on enough horizontal space to keep the relationships visible.
Unlike accordions or dropdowns, there is no single responsive pattern that suits every table. The right approach depends on the data and how users interact with it. Choosing the wrong method can hurt usability for people relying on assistive technology, keyboard navigation, or reduced dexterity.
This article covers the patterns that keep tables usable at smaller viewports, without venturing into search, filtering, or other features.
Accessibility Groundwork For Tables
Before making a table responsive, confirm the basics are in place:
- Use alignment that fits the data type, with clear spacing between rows and cells.
- Style header cells distinctly from data cells.
- Consider zebra striping for rows or columns to ease tracking.
ARIA roles matter when CSS changes the display of a table. Applying display: block or display: flex to create stacked columns can cause browsers to drop table semantics. Adding explicit ARIA roles preserves that structure for screen readers. Manually marking up every table is tedious; for JavaScript-driven sites, a helper function can inject the roles automatically.
Scripted ARIA has trade-offs. The table loses semantics if JavaScript is disabled, the file arrives late over a slow connection, or another script error prevents it from running.
You also want a title close to the table. Use a caption element as the first child of table when possible, nesting a heading inside it to preserve document structure. If a scroll wrapper or other feature makes the caption unsuitable, wrap the table in a figure and tie a figcaption to the table or its wrapper with an ARIA label.
Other accessibility concerns — keyboard behavior, print styles, forced-colors mode — appear in the patterns below. Heydon Pickering's data table guide and Adrian Roselli's responsive table article cover the full range of best practices.
The Minimal Approach: Fluid Width
Simple tables may not need any structural change. Set the width to a responsive value without letting it stretch beyond what the content requires.
Using max-width: fit-content alongside a percentage width keeps the table tight to its contents while still shrinking on narrow screens. This avoids the awkward wide gaps that appear when a small table is forced to width: 100% on a large container — the columns spread apart and scanning falls apart.
For tables with short, unbreakable content, pairing this with fluid typography keeps the output legible without any wrapper elements or scripting.
Horizontal Scroll Pattern
When a table has columns that are essential — comparing values or keeping a strict hierarchy — squeezing it into a small viewport destroys meaning. Instead, let the table be its natural width, and scroll it horizontally.
A wrapper element with overflow: auto adds scrollbars only when the viewport runs short of space. The layout and alignment of the original table remain unchanged.
Keyboard users are left out unless the scrollable region is focusable. Add tabindex to the wrapper or the table so it can be reached and scrolled without a mouse.
Signaling That Scrolling Exists
On touch devices, scrollbars often hide entirely, so there is no visual hint that a table continues off-screen. A technique using gradient backgrounds with background-attachment: local renders shadows at the table edges that fade as you reach the end of the scroll range. The effect communicates both the existence and the direction of the scrollable content.
This approach is worth noting for what it does not add — no wrapper elements and no javascripting. There is one caveat: iOS Safari and certain other browsers do not support background-attachment. If the effect fails, it is only a visual enhancement; the content remains usable, but consider offering text guidance or a fallback style.
Cropping Edge Columns
The shadow trick gives a hint of hidden content, but the final column still looks like the edge of the content. A different hand-off is to actively crop the rightmost column once the viewport gets tight. A small function can set that column's width relative to its natural size, and hide the column entirely if only a small sliver would remain visible. Users see data truncated mid-cell and recognize immediately that more is reachable through scrolling.
Keeping Headers In View
When content scrolls out of view, the headers vanish before the data that depends on them does. With the header row set to position: sticky, the table maintains a visual anchor while scrolling horizontally or vertically. Sticky positioning on a thead row is benign on longer tables where no scrolling occurs; make sure the styling that fixes background and borders on the sticky row is applied regardless, since a transparent sticky header makes the data scroll beneath it look messy.
Pairing the sticky header with the scrolling shadows gives users both orientation for the data currently beneath the cursor and a sense of the range left to travel.
Turning Rows Into Stacked Blocks
The stacking pattern has been a staple of responsive table design for years. It transforms each row into a vertical stack of columns, which works well when the data lacks a strict hierarchy or when users typically scan individual items rather than compare them side by side. Examples include webshop cart items or a contact list where each entry is self-contained.
The conventional implementation applies display: block to table elements on small screens. However, as Adrian Roselli has pointed out, overriding the display property can strip native table semantics from the element, diminishing accessibility for screen reader users. The good news is that browser support has improved. Since Chrome 80, HTML tables no longer lose semantics when using flex, grid, inline-block, or contents; the Chromium-based Edge follows suit. Firefox still drops semantics for display: contents only, while Safari drops them for all display changes.
For this reason, a pattern using display: flex is preferable to display: block for stacking, even though cross-browser behavior still requires testing.
/* Small screen width styles */
table, tbody, tbody tr, tbody td, caption {
display: flex;
flex-direction: column;
width: 100%;
word-break: break-all;
}
See the Pen [Table - stacked [forked]](https://codepen.io/smashingmag/pen/bGKBNNr) by Adrian Bece.
Compacting With An Accordion
One downside of the full stacking pattern is that it can drastically increase the page height. If a table sits above other content, users may need to scroll excessively. A practical refinement is to show only the primary column—typically the first one—and tuck the remaining details into an accordion. This fits scenarios where users first locate a name and then drill into the specifics for that row.
<tr>
<td onclick="toggle()">
<button aria-label="Expand contact details">
<!-- Icon -->
</button>
<!-- Main content-->
</td>
<td><!-- Secondary content--></td>
<td><!-- Secondary content--></td>
<td><!-- Secondary content--></td>
</tr>
The assumption is that the first column holds the key identifier, and the other columns remain hidden unless the row carries a row-active class.
/* Small screen width styles */
thead tr > *:not(:first-child) {
display: none;
}
tbody,
tbody tr,
tbody td {
display: flex;
flex-direction: column;
word-break: break-all;
}
tbody td:first-child {
flex-direction: row;
align-items: center;
}
tbody tr:not(.row-active) > *:not(:first-child) {
max-width: 0;
max-height: 0;
overflow: hidden;
padding: 0;
}
Screen reader support here means managing the aria-hidden state. If the secondary content is toggled via display, the ARIA property does not need separate handling since hidden elements are automatically removed from the accessibility tree.
function toggle() {
const row = this.window.event.target.closest("tr");
row.classList.toggle("row-active");
const isActive = row.classList.contains("row-active");
if (isActive) {
const activeColumns = row.querySelectorAll("td:not(:first-child)");
activeColumns.forEach(function (col) {
col.setAttribute("aria-hidden", "false");
});
} else {
const activeColumns = row.querySelectorAll(`td[aria-hidden="false"]`);
activeColumns.forEach(function (col) {
col.setAttribute("aria-hidden", "true");
});
}
Attaching the toggle function to the onclick handler of the primary column elements makes the entire column clickable. When the window resizes between viewport modes, the correct ARIA labels must be reapplied during initialization and resize events to avoid stale states.
function handleResize() {
const isMobileMode = window.matchMedia("screen and (max-width: 880px)");
const inactiveColumns = document.querySelectorAll(
"tbody > tr > td:not(:first-child)"
);
inactiveColumns.forEach(function (col) {
col.setAttribute("aria-hidden", isMobileMode.matches.toString());
});
}
//On window resize
window.addEventListener("resize", handleResize);
// On document load
handleResize();
See the Pen [Table - accordion [forked]](https://codepen.io/smashingmag/pen/dyKOYVr) by Adrian Bece.
Compared to the complete stacking approach, this accordion variant yields a noticeably shorter table on small screens, keeping the content below quickly reachable.
Offering Toggleable Columns
Another way to handle complex tables is to let users customize their view by showing or hiding individual columns. This is helpful when people want to focus on a few specific fields for scanning or comparison without abandoning the row-and-column layout altogether.
The implementation uses checkbox inputs that call a JavaScript function with the target column index. Both the header cell and the corresponding cells in the table body need to be hidden for consistency.
function toggleRow(index) {
// Hide a data column for all rows in the table body.
allBodyRows.forEach(function (row) {
const cell = row.querySelector(`td:nth-child(${index + 1})`);
cell.classList.toggle("hidden");
});
// Hide a table header element.
allHeadCols[index].classList.toggle("hidden");
}
This approach sidesteps the stacking pattern entirely, preserving an easy comparison view while giving users control over table complexity. Since the visibility toggling relies on display, there is no need to manage ARIA attributes manually.
See the Pen [Responsive table - column toggle [forked]](https://codepen.io/smashingmag/pen/RwJoWQb) by Adrian Bece.
Where This Leaves Us
There is no one-size-fits-all solution for responsive tables. The right pattern depends on how the data is used, whether that is scanning a single item, comparing rows, or quickly skipping past the table. Simple layout changes, such as enabling horizontal scroll or stacking rows into blocks, cover many cases. Adding JavaScript-driven interactions—like accordion details or toggleable columns—handles more complex demands where user control is beneficial.
Part 2 will explore additional patterns and look at responsive table libraries that build in features such as filtering and pagination.



