Open demo

CSS code

HTML
<h2 class="sh-title" id="sh-title">Top pages this month</h2>
<p class="sh-sub">Scroll the list — the header row stays pinned with <code>position: sticky</code>. No JavaScript.</p>
<div class="sh-scroll" role="region" tabindex="0" aria-label="Top pages this month, scrollable">
  <table class="sh" aria-labelledby="sh-title">
    <thead><tr><th scope="col">Page</th><th scope="col">Views</th><th scope="col">Avg. time</th><th scope="col">Source</th></tr></thead>
    <tbody>
        <tr><th scope="row">/pricing</th><td class="k">48,102</td><td class="k">1:42</td><td><span class="sh-tag">Organic</span></td></tr>
        <tr><th scope="row">/blog/css-grid-guide</th><td class="k">36,880</td><td class="k">6:18</td><td><span class="sh-tag">Organic</span></td></tr>
        <tr><th scope="row">/</th><td class="k">31,204</td><td class="k">0:58</td><td><span class="sh-tag">Direct</span></td></tr>
        <tr><th scope="row">/docs/quickstart</th><td class="k">22,417</td><td class="k">4:05</td><td><span class="sh-tag">Referral</span></td></tr>
        <tr><th scope="row">/changelog</th><td class="k">18,932</td><td class="k">2:11</td><td><span class="sh-tag">Direct</span></td></tr>
        <tr><th scope="row">/blog/dark-mode-tokens</th><td class="k">15,309</td><td class="k">5:27</td><td><span class="sh-tag">Social</span></td></tr>
        <tr><th scope="row">/templates</th><td class="k">12,761</td><td class="k">3:33</td><td><span class="sh-tag">Organic</span></td></tr>
        <tr><th scope="row">/docs/api-reference</th><td class="k">11,048</td><td class="k">7:51</td><td><span class="sh-tag">Direct</span></td></tr>
        <tr><th scope="row">/about</th><td class="k">8,414</td><td class="k">1:07</td><td><span class="sh-tag">Organic</span></td></tr>
        <tr><th scope="row">/careers</th><td class="k">6,822</td><td class="k">2:44</td><td><span class="sh-tag">Social</span></td></tr>
        <tr><th scope="row">/blog/a11y-checklist</th><td class="k">5,933</td><td class="k">8:02</td><td><span class="sh-tag">Referral</span></td></tr>
        <tr><th scope="row">/contact</th><td class="k">4,168</td><td class="k">0:49</td><td><span class="sh-tag">Direct</span></td></tr>
    </tbody>
  </table>
</div>
CSS
/* Sticky header table: the header cells are position:sticky inside a scroll box with a max-height.
   border-collapse:separate keeps the header's bottom border attached while it sticks.
   The scroll box is a focusable, labelled region so keyboard users can scroll it too. */
.sh-title{font-size:1.15rem;font-weight:700;letter-spacing:-.01em;margin:0 0 4px}
.sh-sub{color:#686280;font-size:.87rem;margin:0 0 20px}
.sh-scroll{max-height:330px;overflow:auto;background:#fff;border:1px solid #e8e5f1;border-radius:14px;box-shadow:0 12px 30px -18px rgba(41,32,77,.18)}
.sh-scroll:focus-visible{outline:2px solid #6d28d9;outline-offset:2px}
.sh{width:100%;border-collapse:separate;border-spacing:0;font-size:.88rem}
.sh th,.sh td{padding:12px 16px;text-align:left}
.sh thead th{position:sticky;top:0;z-index:1;background:#fff;font-size:.73rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:#686280;border-bottom:2px solid #ece8f5}
.sh tbody th,.sh td{border-bottom:1px solid #f2f0f8;color:#40395c}
.sh tbody tr:hover>*{background:#faf8fd}
.sh tbody th{font-weight:600;color:#241f36}
.sh .k{font-variant-numeric:tabular-nums}
.sh-tag{font-size:.72rem;font-weight:700;padding:3px 10px;border-radius:999px;background:#efeaff;color:#6d28d9}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<h2 class="sh-title" id="sh-title">Top pages this month</h2>
<p class="sh-sub">Scroll the list — the header row stays pinned with <code>position: sticky</code>. No JavaScript.</p>
<div class="sh-scroll overflow-auto" role="region" tabindex="0" aria-label="Top pages this month, scrollable">
  <table class="table table-hover sh align-middle mb-0" aria-labelledby="sh-title">
    <thead class="sticky-top"><tr><th scope="col">Page</th><th scope="col">Views</th><th scope="col">Avg. time</th><th scope="col">Source</th></tr></thead>
    <tbody>
        <tr><th scope="row">/pricing</th><td class="k">48,102</td><td class="k">1:42</td><td><span class="badge rounded-pill">Organic</span></td></tr>
        <tr><th scope="row">/blog/css-grid-guide</th><td class="k">36,880</td><td class="k">6:18</td><td><span class="badge rounded-pill">Organic</span></td></tr>
        <tr><th scope="row">/</th><td class="k">31,204</td><td class="k">0:58</td><td><span class="badge rounded-pill">Direct</span></td></tr>
        <tr><th scope="row">/docs/quickstart</th><td class="k">22,417</td><td class="k">4:05</td><td><span class="badge rounded-pill">Referral</span></td></tr>
        <tr><th scope="row">/changelog</th><td class="k">18,932</td><td class="k">2:11</td><td><span class="badge rounded-pill">Direct</span></td></tr>
        <tr><th scope="row">/blog/dark-mode-tokens</th><td class="k">15,309</td><td class="k">5:27</td><td><span class="badge rounded-pill">Social</span></td></tr>
        <tr><th scope="row">/templates</th><td class="k">12,761</td><td class="k">3:33</td><td><span class="badge rounded-pill">Organic</span></td></tr>
        <tr><th scope="row">/docs/api-reference</th><td class="k">11,048</td><td class="k">7:51</td><td><span class="badge rounded-pill">Direct</span></td></tr>
        <tr><th scope="row">/about</th><td class="k">8,414</td><td class="k">1:07</td><td><span class="badge rounded-pill">Organic</span></td></tr>
        <tr><th scope="row">/careers</th><td class="k">6,822</td><td class="k">2:44</td><td><span class="badge rounded-pill">Social</span></td></tr>
        <tr><th scope="row">/blog/a11y-checklist</th><td class="k">5,933</td><td class="k">8:02</td><td><span class="badge rounded-pill">Referral</span></td></tr>
        <tr><th scope="row">/contact</th><td class="k">4,168</td><td class="k">0:49</td><td><span class="badge rounded-pill">Direct</span></td></tr>
    </tbody>
  </table>
</div>
CSS
/* Bootstrap .table-hover inside an .overflow-auto box; .sticky-top pins the header row.
   border-collapse:separate keeps the header's bottom border attached while it sticks. */
.sh-title{font-size:1.15rem;font-weight:700;letter-spacing:-.01em;line-height:normal;margin:0 0 4px}
.sh-sub{color:#686280;font-size:.87rem;margin:0 0 20px}
.sh-sub code{color:inherit}
.sh-scroll{max-height:330px;background:#fff;border:1px solid #e8e5f1;border-radius:14px;box-shadow:0 12px 30px -18px rgba(41,32,77,.18)}
.sh-scroll:focus-visible{outline:2px solid #6d28d9;outline-offset:2px}
.sh{--bs-table-bg:#fff;--bs-table-color:#40395c;--bs-table-border-color:#f2f0f8;--bs-table-hover-bg:#faf8fd;--bs-table-hover-color:#40395c;
  border-collapse:separate;border-spacing:0;font-size:.88rem}
.sh>:not(caption)>*>*{padding:12px 16px}
.sh>thead{z-index:1}
.sh>thead>tr>th{font-size:.73rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;--bs-table-color:#686280;border-bottom:2px solid #ece8f5}
.sh>tbody>tr:last-child>*{border-bottom-width:0}
.sh tbody th{font-weight:600;--bs-table-color:#241f36;--bs-table-hover-color:#241f36}
.sh .k{font-variant-numeric:tabular-nums}
.sh .badge{--bs-badge-padding-x:10px;--bs-badge-padding-y:3px;--bs-badge-font-size:.72rem;--bs-badge-font-weight:700;display:inline;line-height:normal;background:#efeaff;color:#6d28d9}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<h2 id="sh-title" class="mb-1 text-[1.15rem] font-bold tracking-[-.01em]">Top pages this month</h2>
<p class="mb-5 text-[.87rem] text-[#686280]">Scroll the list — the header row stays pinned with <code>position: sticky</code>. No JavaScript.</p>
<div role="region" tabindex="0" aria-label="Top pages this month, scrollable" class="max-h-[330px] overflow-auto rounded-[14px] border border-[#e8e5f1] bg-white shadow-[0_12px_30px_-18px_rgba(41,32,77,.18)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#6d28d9]">
  <table aria-labelledby="sh-title" class="w-full border-separate border-spacing-0 text-[.88rem] [&_td]:px-4 [&_td]:py-3 [&_th]:px-4 [&_th]:py-3 [&_th]:text-left">
    <thead class="text-[.73rem] font-bold tracking-[.05em] text-[#686280] uppercase [&_th]:sticky [&_th]:top-0 [&_th]:z-[1] [&_th]:border-b-2 [&_th]:border-[#ece8f5] [&_th]:bg-white"><tr><th scope="col">Page</th><th scope="col">Views</th><th scope="col">Avg. time</th><th scope="col">Source</th></tr></thead>
    <tbody class="text-[#40395c] [&_td]:border-b [&_td]:border-[#f2f0f8] [&_th]:border-b [&_th]:border-[#f2f0f8] [&_th]:font-semibold [&_th]:text-[#241f36] [&>tr:hover>*]:bg-[#faf8fd]">
        <tr><th scope="row">/pricing</th><td class="tabular-nums">48,102</td><td class="tabular-nums">1:42</td><td><span class="rounded-full bg-[#efeaff] px-2.5 py-[3px] text-[.72rem] font-bold text-[#6d28d9]">Organic</span></td></tr>
        <tr><th scope="row">/blog/css-grid-guide</th><td class="tabular-nums">36,880</td><td class="tabular-nums">6:18</td><td><span class="rounded-full bg-[#efeaff] px-2.5 py-[3px] text-[.72rem] font-bold text-[#6d28d9]">Organic</span></td></tr>
        <tr><th scope="row">/</th><td class="tabular-nums">31,204</td><td class="tabular-nums">0:58</td><td><span class="rounded-full bg-[#efeaff] px-2.5 py-[3px] text-[.72rem] font-bold text-[#6d28d9]">Direct</span></td></tr>
        <tr><th scope="row">/docs/quickstart</th><td class="tabular-nums">22,417</td><td class="tabular-nums">4:05</td><td><span class="rounded-full bg-[#efeaff] px-2.5 py-[3px] text-[.72rem] font-bold text-[#6d28d9]">Referral</span></td></tr>
        <tr><th scope="row">/changelog</th><td class="tabular-nums">18,932</td><td class="tabular-nums">2:11</td><td><span class="rounded-full bg-[#efeaff] px-2.5 py-[3px] text-[.72rem] font-bold text-[#6d28d9]">Direct</span></td></tr>
        <tr><th scope="row">/blog/dark-mode-tokens</th><td class="tabular-nums">15,309</td><td class="tabular-nums">5:27</td><td><span class="rounded-full bg-[#efeaff] px-2.5 py-[3px] text-[.72rem] font-bold text-[#6d28d9]">Social</span></td></tr>
        <tr><th scope="row">/templates</th><td class="tabular-nums">12,761</td><td class="tabular-nums">3:33</td><td><span class="rounded-full bg-[#efeaff] px-2.5 py-[3px] text-[.72rem] font-bold text-[#6d28d9]">Organic</span></td></tr>
        <tr><th scope="row">/docs/api-reference</th><td class="tabular-nums">11,048</td><td class="tabular-nums">7:51</td><td><span class="rounded-full bg-[#efeaff] px-2.5 py-[3px] text-[.72rem] font-bold text-[#6d28d9]">Direct</span></td></tr>
        <tr><th scope="row">/about</th><td class="tabular-nums">8,414</td><td class="tabular-nums">1:07</td><td><span class="rounded-full bg-[#efeaff] px-2.5 py-[3px] text-[.72rem] font-bold text-[#6d28d9]">Organic</span></td></tr>
        <tr><th scope="row">/careers</th><td class="tabular-nums">6,822</td><td class="tabular-nums">2:44</td><td><span class="rounded-full bg-[#efeaff] px-2.5 py-[3px] text-[.72rem] font-bold text-[#6d28d9]">Social</span></td></tr>
        <tr><th scope="row">/blog/a11y-checklist</th><td class="tabular-nums">5,933</td><td class="tabular-nums">8:02</td><td><span class="rounded-full bg-[#efeaff] px-2.5 py-[3px] text-[.72rem] font-bold text-[#6d28d9]">Referral</span></td></tr>
        <tr><th scope="row">/contact</th><td class="tabular-nums">4,168</td><td class="tabular-nums">0:49</td><td><span class="rounded-full bg-[#efeaff] px-2.5 py-[3px] text-[.72rem] font-bold text-[#6d28d9]">Direct</span></td></tr>
    </tbody>
  </table>
</div>

About this set

A scrolling list of top pages whose header row stays pinned while the body scrolls under it. The table lives in a box with a 330 pixel max-height and overflow:auto, and each header cell has position:sticky with top:0 and a white background. The table uses border-collapse:separate with zero spacing, which keeps the header’s bottom border attached while it sticks; with collapsed borders the line would scroll away. The first column holds the page path as a row header, numbers use tabular figures so they line up, and a purple tag shows the traffic source. Use it for long lists inside dashboards and cards where the page itself should not scroll: analytics tables, logs and search results. The scroll box is a region with an aria-label and tabindex=”0″, so keyboard users can focus it and scroll with the arrow keys, and it also scrolls sideways on phones. No JavaScript is involved. The Bootstrap version puts .sticky-top on the thead inside an .overflow-auto box, and the Tailwind version uses sticky top-0 on the header cells.

What’s included

  • Header row pinned with position:sticky, no JavaScript
  • border-collapse:separate so the header border sticks too
  • Tabular numerals for aligned figures
  • Focusable, labelled scroll box for keyboard scrolling
  • Bootstrap .sticky-top and Tailwind sticky top-0 versions

Accessibility

The table is labelled by its heading and uses scope on column and row headers. The scroll box is a named region with tabindex="0" and a 2px purple focus outline, so it can be scrolled from the keyboard. Header and body text meet 4.5:1 contrast.

Customise it

Change max-height on .sh-scroll for the visible height, and the colours in the .sh rules (header text #686280, hover #faf8fd, tag #efeaff and #6d28d9). Bootstrap uses --bs-table-* variables on .sh; in Tailwind edit max-h-[330px] and the colour utilities.