How to build a scrollable table with a sticky header using Tailwind CSS
Create a vertically scrollable table with a sticky header using Tailwind CSS. Includes alignment tips, zebra striping, and a full working demo.
Published on October 31, 2025 by Michael AndreuzzaLet’s build a scrollable table with a sticky header
Sometimes your table needs a fixed header while the body scrolls. This pattern is perfect for dashboards and list views: it keeps column labels visible and the content tidy.
Heads up: You only need Tailwind CSS for this one. We’ll use
position: stickyon the<thead>and a scrollable container for the body.
1. Outer wrapper
Wrap the table in a card-like container for borders, rounding, and clipping:
<div
class="w-full max-w-5xl mx-auto overflow-hidden bg-white border rounded-lg border-zinc-200"
>
<!-- Scroll area and table go here -->
...
</div>
overflow-hiddenkeeps the rounded corners clean while the header sticksborder+rounded-lggives the table a clean, contained look
2. Scroll area
The scrollable region wraps the table and sets the vertical max height:
<div class="overflow-auto max-h-80">
<!-- table -->
</div>
overflow-autoenables scrolling when content exceedsmax-h-80- Adjust height with Tailwind’s spacing scale (e.g.,
max-h-64,max-h-[420px])
3. Sticky header
Apply sticky positioning to the header, keep it above rows, and give it a solid background to cover content beneath.
<table class="min-w-full text-xs bg-white" role="table">
<thead class="sticky top-0 z-10 bg-zinc-50 text-zinc-600" role="rowgroup">
<tr role="row">
<th scope="col" class="px-3 py-2 font-medium text-left">ID</th>
<th scope="col" class="px-3 py-2 font-medium text-left">Title</th>
<th scope="col" class="px-3 py-2 font-medium text-left">Owner</th>
<th scope="col" class="px-3 py-2 font-medium text-left">Updated</th>
<th scope="col" class="px-3 py-2 font-medium text-right">Items</th>
</tr>
</thead>
<!-- tbody here -->
</table>
sticky top-0pins the<thead>to the top of the scroll containerz-10ensures the header sits above body rowsbg-zinc-50avoids transparency as rows scroll underneath
How sticky actually works
- Sticky uses the nearest scrolling ancestor as its reference. Here, that’s the
divwithoverflow-auto. - The sticky element must be inside the scrolling container to “stick”. If
overflow-autolives on a different ancestor, the header won’t stick. - Give the sticky region an opaque background and a stacking order (
z-10) so it visually floats over rows.
4. Body rows and alignment
For readability, keep numeric columns right-aligned, enable tabular figures, and add zebra striping:
<tbody>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1001</td>
<td class="px-3 py-2 text-zinc-700">Collection 1</td>
<td class="px-3 py-2 text-zinc-500">owner0@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-01</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">0</td>
</tr>
<!-- Repeat rows -->
</tbody>
even:bg-zinc-50creates zebra stripingfont-mono tabular-numsdelivers clean, aligned numbers- Keep padding consistent (
px-3 py-2) across cells
Column sizing that won’t jump
- The default
table-layout: autosizes columns based on content; it’s convenient but can reflow if content changes. - For predictable widths (especially with long strings), set
table-layout: fixedand add explicit widths on columns or cells when needed.
<table class="min-w-full text-xs bg-white table-fixed">
<!-- Add fixed or proportional widths via utilities, e.g., w-1/3, w-24 -->
</table>
5. Accessibility touches
- Add
role="table"on<table>andscope="col"on<th> - Consider a
<caption>to describe the dataset - For long lists, you can add
aria-rowcounton<table>if known
Optional: Horizontal scroll for many columns
If your columns overflow, wrap the table in an overflow-x-auto container and set a min-w-[...] on the table:
<div class="overflow-x-auto">
<table class="min-w-[960px] text-xs">
...
</table>
</div>
- Pair horizontal scroll with
table-fixedif you want stable column widths across pages. - Add a sticky first column (see below) to keep identifiers visible while scrolling sideways.
Common pitfalls
- Sticky not working? Ensure the sticky element is inside the scrolling container and has
top-0 - Header appears behind rows? Add
z-10(and a background color) to the<thead> - Clipped corners? Keep
overflow-hiddenon the outer card wrapper, not the scroll area
Quick debug checklist
- Is
overflow-autoon the same ancestor that contains the<thead>? If not, move it. - Does
<thead>havebg-*andz-10? Add both to avoid bleed-through. - Using transforms/filters on ancestors? Remove them temporarily; they can change stacking contexts and layering.
Advanced: Sticky first column
Keep the first column visible while scrolling horizontally. Add sticky left-0 and a background on the first header and body cells; use z-20/z-10 so they layer correctly.
<thead class="sticky top-0 z-10 bg-zinc-50 text-zinc-600">
<tr>
<th class="sticky left-0 z-20 bg-zinc-50 px-3 py-2 text-left">ID</th>
<th class="px-3 py-2 text-left">Title</th>
<!-- ... -->
</tr>
</thead>
<tbody>
<tr>
<td class="sticky left-0 z-10 bg-white px-3 py-2">#1001</td>
<td class="px-3 py-2">Collection 1</td>
<!-- ... -->
</tr>
</tbody>
- Use a higher
z-indexon the header cell (e.g.,z-20) so it layers above the sticky body cell at intersections. - Always set a background color on sticky cells so underlying content doesn’t show through.
Bonus: Complete working demo
Copy, paste, and customize:
<div
class="w-full max-w-5xl mx-auto overflow-hidden bg-white border rounded-lg border-zinc-200"
>
<div class="overflow-auto max-h-80">
<table class="min-w-full text-xs bg-white">
<thead class="sticky top-0 z-10 bg-zinc-50 text-zinc-600">
<tr>
<th class="px-3 py-2 font-medium text-left">ID</th>
<th class="px-3 py-2 font-medium text-left">Title</th>
<th class="px-3 py-2 font-medium text-left">Owner</th>
<th class="px-3 py-2 font-medium text-left">Updated</th>
<th class="px-3 py-2 font-medium text-right">Items</th>
</tr>
</thead>
<tbody>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1001</td>
<td class="px-3 py-2 text-zinc-700">Collection 1</td>
<td class="px-3 py-2 text-zinc-500">owner0@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-01</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
0
</td>
</tr>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1002</td>
<td class="px-3 py-2 text-zinc-700">Collection 2</td>
<td class="px-3 py-2 text-zinc-500">owner1@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-02</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
3
</td>
</tr>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1003</td>
<td class="px-3 py-2 text-zinc-700">Collection 3</td>
<td class="px-3 py-2 text-zinc-500">owner2@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-03</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
6
</td>
</tr>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1004</td>
<td class="px-3 py-2 text-zinc-700">Collection 4</td>
<td class="px-3 py-2 text-zinc-500">owner3@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-04</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
9
</td>
</tr>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1005</td>
<td class="px-3 py-2 text-zinc-700">Collection 5</td>
<td class="px-3 py-2 text-zinc-500">owner4@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-05</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
12
</td>
</tr>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1006</td>
<td class="px-3 py-2 text-zinc-700">Collection 6</td>
<td class="px-3 py-2 text-zinc-500">owner5@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-06</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
15
</td>
</tr>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1007</td>
<td class="px-3 py-2 text-zinc-700">Collection 7</td>
<td class="px-3 py-2 text-zinc-500">owner6@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-07</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
18
</td>
</tr>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1008</td>
<td class="px-3 py-2 text-zinc-700">Collection 8</td>
<td class="px-3 py-2 text-zinc-500">owner0@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-08</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
21
</td>
</tr>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1009</td>
<td class="px-3 py-2 text-zinc-700">Collection 9</td>
<td class="px-3 py-2 text-zinc-500">owner1@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-09</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
24
</td>
</tr>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1010</td>
<td class="px-3 py-2 text-zinc-700">Collection 10</td>
<td class="px-3 py-2 text-zinc-500">owner2@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-10</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
27
</td>
</tr>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1011</td>
<td class="px-3 py-2 text-zinc-700">Collection 11</td>
<td class="px-3 py-2 text-zinc-500">owner3@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-11</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
30
</td>
</tr>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1012</td>
<td class="px-3 py-2 text-zinc-700">Collection 12</td>
<td class="px-3 py-2 text-zinc-500">owner4@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-12</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
33
</td>
</tr>
<tr class="even:bg-zinc-50">
<td class="px-3 py-2 text-zinc-500">#1013</td>
<td class="px-3 py-2 text-zinc-700">Collection 13</td>
<td class="px-3 py-2 text-zinc-500">owner5@example.com</td>
<td class="px-3 py-2 text-zinc-500">2025-09-13</td>
<td class="px-3 py-2 font-mono text-right text-zinc-700 tabular-nums">
36
</td>
</tr>
</tbody>
</table>
</div>
</div> /Michael Andreuzza







