Blog
How to Create a Responsive Table with HTML and CSS
Wide data tables are useful on desktop, but their columns can become cramped on phones. This tutorial shows you how to create an accessible HTML table that keeps its familiar rows on larger screens and turns each row into a labeled card on small screens using CSS only.
Quick answer: Build a semantic table with <thead>, <th scope="col">, and <td> cells. Add a data-label to each cell, then use a mobile media query to hide the visual header and display each row as a card with its label from content: attr(data-label).
What you’ll build
The example uses a team directory with four columns. On wider screens it remains a standard table. Below 640 pixels, each person becomes a card with the field names shown beside their values. The data stays in the document as a real table; CSS changes only its presentation.
Why use a semantic table?
Tables are meant for data with relationships between rows and columns. Screen readers can announce header and cell associations when you use table markup correctly. Avoid replacing the table with generic <div> elements just to make the mobile layout easier. The scope="col" attribute makes each column heading explicit.
Step 1: Write the HTML table
Create a caption that describes the data, column headers inside <thead>, and data rows inside <tbody>. Each cell receives a data-label whose value matches its heading. The live demo and source code below use the same structure.
Step 2: Style the desktop table
Use border-collapse for clean borders, comfortable cell padding, and a restrained header background. Keep the table inside an overflow wrapper. This gives users a horizontal-scroll fallback if a wide table cannot reasonably become cards.
Step 3: Turn rows into cards on mobile
At the chosen breakpoint, hide the visual table header accessibly, display each body row as a card, and render each cell’s label with ::before. Using display: grid for the cell keeps labels and values aligned without fragile absolute positioning or large fixed padding.
Customize the responsive table
- Breakpoint: Change 640px to the width at which your actual columns stop fitting.
- Labels: Keep every
data-labelsynchronized with its column heading. - Wide datasets: Keep the overflow wrapper and allow horizontal scrolling when card layout would be harder to scan.
- Colors: Check text, header, borders, and keyboard focus contrast against your theme.
Accessibility and usability tips
Use real table headers and a useful caption. Do not remove the table structure from the accessibility tree. Make sure card labels remain visible when CSS is enabled and the original header remains available to assistive technology. Test with long values, zoom, keyboard navigation, and a narrow viewport. Avoid putting essential information only in color or icons.
Troubleshooting
- Labels are missing on mobile: Confirm each data cell has a matching
data-labeland the mobile media query applies. - Columns are cramped: Lower the breakpoint or use the overflow wrapper at intermediate widths.
- Labels and values collide: Use a two-column grid on each cell and allow values to wrap with
overflow-wrap: anywhere. - Header is announced twice: Ensure the mobile-only visually-hidden rule is applied to the actual
<thead>and that the table headers retain their associations.
Conclusion
A responsive table does not need JavaScript. Semantic HTML preserves the data relationships, while a CSS media query presents the same information as desktop rows or mobile cards. Try the demo, then download the complete example and adapt the fields, breakpoint, and colors to your project.
Reviewed and updated: October 5, 2026.
HTML + CSS
Team directory
Resize this preview to see the table change into labeled cards.
| Name | Department | Role | Location |
|---|---|---|---|
| Ava Patel | Design | Product designer | Toronto |
| Noah Williams | Engineering | Frontend developer | Melbourne |
| Mia Chen | Support | Customer specialist | Singapore |
| Leo Garcia | Engineering | Accessibility engineer | Austin |
The HTML remains a real data table. CSS changes its presentation at the mobile breakpoint.
<section class="team-directory" aria-labelledby="team-title">
<h2 id="team-title">Team directory</h2>
<div class="table-wrap">
<table>
<caption class="visually-hidden">Team members and their roles</caption>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Department</th>
<th scope="col">Role</th>
<th scope="col">Location</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Name">Ava Patel</td>
<td data-label="Department">Design</td>
<td data-label="Role">Product designer</td>
<td data-label="Location">Toronto</td>
</tr>
</tbody>
</table>
</div>
</section>
.team-directory {
color: #17233d;
font: 16px/1.55 system-ui, sans-serif;
}
.table-wrap {
overflow-x: auto;
border: 1px solid #dce5f2;
border-radius: 14px;
}
table {
width: 100%;
border-collapse: collapse;
text-align: left;
}
th, td {
padding: 15px 18px;
border-bottom: 1px solid #dce5f2;
}
th {
background: #edf3ff;
color: #263e70;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media (max-width: 640px) {
.table-wrap { overflow: visible; border: 0; }
table, tbody { display: block; }
thead {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
tbody tr {
display: grid;
gap: 10px;
margin-bottom: 14px;
padding: 15px;
border: 1px solid #dce5f2;
border-radius: 14px;
background: #fff;
}
tbody td {
display: grid;
grid-template-columns: 105px minmax(0, 1fr);
gap: 12px;
padding: 0;
border: 0;
overflow-wrap: anywhere;
}
tbody td::before {
content: attr(data-label);
color: #5b6880;
font-size: .78rem;
font-weight: 700;
text-transform: uppercase;
}
}