Keeps a long table's header row visible while the rows scroll, and freezes up to three leading columns while it scrolls sideways — Excel's freeze panes, for an HTML table — by wrapping the <table> you already have, with no change to a single <th>. Reach for it wherever a table is taller than the screen and the column names stop being visible a third of the way down: an admin users, accounts or customers list, an orders, invoices or billing-history table, a transactions or ledger view, logs and audit trails, an inventory or stock table, a reporting or analytics breakdown, a data grid on a dashboard, a pricing or comparison matrix, a spreadsheet-like editor, search results, a tickets or issues queue, and any wide table people scroll sideways through. Common asks it answers: "sticky table header react", "shadcn sticky table header", "shadcn table header not sticking", "freeze header row table css", "frozen header table react", "table header stays on scroll", "position sticky th not working", "sticky thead not working", "sticky top-0 does nothing on table header", "why is my sticky header not sticking", "sticky table header border disappears", "border bottom missing on sticky header", "sticky header rows show through", "table rows visible through header", "transparent sticky header table", "freeze first column table react", "sticky first column and header", "sticky column overlaps header", "corner cell z-index table", "pin multiple columns table", "sticky header under fixed navbar", "table header hidden behind app header", "sticky header offset fixed navbar", "scrollable table with fixed header", "table with max height and sticky header", "tanstack table sticky header", "data table sticky header shadcn", "react-table sticky header css", "freeze panes html table", "excel freeze panes react", "freeze top row and first column", "tailwind sticky table header not working", "sticky header border-collapse", "sticky thead inside overflow-x-auto", "sticky header inside a scrollable div", "shadcn table wrapper breaks sticky", "テーブル ヘッダー 固定 react", "見出し行 固定 スクロール", "表 ヘッダ 固定 shadcn", "列固定 テーブル react", "先頭列 固定 横スクロール". Official shadcn/ui has nothing here, and the measurement is not close: fetching every entry in its registry today — 63 listed, 62 fetchable, questionnaire is indexed and 404s on both style tracks — and grepping all 223,289 bytes of source, sticky, position: sticky, position:sticky, sticky top-0, stickyHeader and scrollLeft are every one of them a zero hit, as are getBoundingClientRect, ResizeObserver, IntersectionObserver and offsetHeight, so nothing in that registry measures an element at all. Its table is 2,468 bytes of six styling wrappers in which the word sticky never appears. So this gets hand-rolled, and four things go wrong every time — three of them invisible until you actually scroll. The first is that the border under the header disappears. Tailwind's preflight sets border-collapse: collapse on every <table>, in v3 and v4 alike, and in the collapsed model a border belongs to the table's grid rather than to the cell that declared it. The cell is what sticks; the grid is not — so border-b on a pinned <th> scrolls away with the rows and the header is left with no bottom edge, floating over the data. The line here is an ::after box, which belongs to the cell and travels with it. And the repair the search results hand you for that — flip the table to border-separate so the borders belong to the cells — is the one thing this refuses to do, because the separated model ignores borders on a <tr> and shadcn's own TableRow puts its border-b exactly there. Flipping the table trades one missing line in the header for every missing divider in the body, which is a much larger regression arriving as a fix. The second is that the header is transparent. A <th> has no background of its own, so a pinned one does not stay in front of the rows, it stays in place while they pass through it, and the heading and whatever row is underneath are painted over each other. It reads as a rendering fault rather than as a missing colour, which is how it survives review. The third is the one that produces "sticky just does not work": a sticky cell is pinned to its nearest scrolling ancestor, never to the window unless the window is that ancestor, so the question is never whether you wrote sticky top-0 but which element is the scrollport. Official shadcn/ui's own <Table> is the usual culprit — it renders <div class="relative w-full overflow-x-auto"> around the table, and a box with overflow-x: auto computes overflow-y: auto as well, so that div is a scrollport on both axes and a header pinned to it never moves, because it is the page that is scrolling. Pass maxHeight and this wrapper becomes the scrollport itself, which always works and is the only arrangement in which a table can scroll sideways and keep a pinned header at the same time; leave it off and the page scrolls, in which case nothing between this wrapper and the <table> may have an overflow. The fourth is the corner. Pinned columns need a left that starts where the previous column ended, which cannot be a static class because it is the measured width of the column before it — pinning them all at left: 0 stacks three columns in the space of one, each painting its text through the others — and a sticky cell makes its own stacking context, so a header cell and a body cell that tie on z-index are resolved by document order, which puts the body on top and slides the first column over the heading as soon as the table moves sideways. stickyColumns pins up to three leading columns with offsets measured through a ResizeObserver, and lifts the corner above both. offset takes the height of a fixed app header, without which the table header pins correctly and parks underneath the toolbar for the whole of the scroll. maxHeight takes anything CSS does — 420, "60vh", "calc(100dvh - 12rem)". Recolour the pinned cells with an important utility, className="[&_thead_th]:!bg-card", since these are descendant rules. One header row is supported: a second row of grouping headers would need its own measured offset, and pinning both at the same top would stack them. Within pulld it is what a long table wants next to sort-header, which orders one column; scroll-shadow, which fades the edges so you can tell there is more sideways; virtual-list and infinite-scroll, which deal with how many rows there are rather than where the header is; and sortable-list, which drags rows into an order by hand. One file, your <table> stays yours, and it adds no dependencies at all.
pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/sticky-table-header.json"