feat(datatable): sticky table header row

Make the DataTable's <thead> sticky so the column headers stay put at the
top of the page scroller while the rows scroll under them — the core of
'the table header should be fixed, only the rows scroll'. Benefits every
DataTable page (Channels manager, discovery, Audit log).

Each header cell gets bg-bg so rows don't show through; the header's
bottom border rides an inset box-shadow because a sticky <tr>'s own
border can scroll away with the collapsed table box. Verified on the
Audit log: 500 rows scroll under a header stuck at the scroller top; the
scroller's top fade is unobtrusive behind the solid header. Suite 15/15.
This commit is contained in:
2026-07-18 05:11:10 +02:00
parent 02bd0d433b
commit 7df9bc0035
+7 -3
View File
@@ -350,8 +350,12 @@ export default function DataTable<T>({
{/* Wide screens: table. The wrapper isn't clipped so header filter popovers can overflow. */}
<div className="hidden md:block">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="text-muted border-b border-border">
{/* Sticky header row: the column headers stay put at the top of the page scroller while
the rows scroll under them. `bg-bg` on each cell keeps rows from showing through; the
border rides a pseudo-element (box-shadow) since a sticky <tr>'s own border scrolls
away with the collapsed table box in some browsers. */}
<thead className="sticky top-0 z-10">
<tr className="text-muted [&>th]:shadow-[inset_0_-1px_0_var(--border)]">
{columns.map((col) => {
const sorted_ = sort?.key === col.key;
const filterOn = isActive(filters[col.key]);
@@ -359,7 +363,7 @@ export default function DataTable<T>({
<th
key={col.key}
style={col.width ? { width: col.width } : undefined}
className={`relative font-medium px-2 py-2 whitespace-nowrap ${align(col.align)}`}
className={`relative font-medium px-2 py-2 whitespace-nowrap bg-bg ${align(col.align)}`}
>
<span className="inline-flex items-center gap-1">
<button