Black Weeks. Full Access for 50% OFF. Use code lex50 at checkout.

You'll get every theme we've made — and every one we'll ever make. That's 39 themes total.

Unlimited projects. Lifetime updates. One payment.

Get full access

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 Andreuzza

Let’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: sticky on 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-hidden keeps the rounded corners clean while the header sticks
  • border + rounded-lg gives 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-auto enables scrolling when content exceeds max-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-0 pins the <thead> to the top of the scroll container
  • z-10 ensures the header sits above body rows
  • bg-zinc-50 avoids transparency as rows scroll underneath

How sticky actually works

  • Sticky uses the nearest scrolling ancestor as its reference. Here, that’s the div with overflow-auto.
  • The sticky element must be inside the scrolling container to “stick”. If overflow-auto lives 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-50 creates zebra striping
  • font-mono tabular-nums delivers clean, aligned numbers
  • Keep padding consistent (px-3 py-2) across cells

Column sizing that won’t jump

  • The default table-layout: auto sizes columns based on content; it’s convenient but can reflow if content changes.
  • For predictable widths (especially with long strings), set table-layout: fixed and 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> and scope="col" on <th>
  • Consider a <caption> to describe the dataset
  • For long lists, you can add aria-rowcount on <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-fixed if 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-hidden on the outer card wrapper, not the scroll area

Quick debug checklist

  • Is overflow-auto on the same ancestor that contains the <thead>? If not, move it.
  • Does <thead> have bg-* and z-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-index on 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

Did you like this post? Please share it with your friends!