Sticky Pinning
Pin the toolbar, column header, and group header to the top of the scroll ancestor on long lists. Keeps context visible while scrolling through hundreds of rows.
Overview
The sticky prop keeps the table's three contextual
layers in view while the rows scroll past them: the toolbar (L1), the column header (L2), and the group header (L3, when grouping is active). Each pinned layer
sits below the one above it, so they stack instead of overlapping. If your app shell has a
fixed top bar, stickyOffset moves the whole stack down
by its height.
Columns scroll with their rows; nothing pins to the left or right edge. For a table wider
than the viewport, reach for fit="viewport" (see below):
it contains both axes of scroll inside the table, so the page never scrolls sideways.
Playground
Scroll the page to see the pinned layer(s). Adjust stickyOffset to simulate a fixed app-shell top bar.
Name | Role | Department | Location | |
|---|---|---|---|---|
Platform (16 items) | ||||
Emma Wilson | Staff Engineer | Platform | Berlin | |
Sofia Martinez | Engineering Manager | Platform | Munich | |
Noah Kim | DevOps Engineer | Platform | Hamburg | |
Ethan Müller | QA Engineer | Platform | Hamburg | |
Emma Wilson | Staff Engineer | Platform | Berlin | |
Sofia Martinez | Engineering Manager | Platform | Munich | |
Noah Kim | DevOps Engineer | Platform | Hamburg | |
Ethan Müller | QA Engineer | Platform | Hamburg | |
Emma Wilson | Staff Engineer | Platform | Berlin | |
Sofia Martinez | Engineering Manager | Platform | Munich | |
Noah Kim | DevOps Engineer | Platform | Hamburg | |
Ethan Müller | QA Engineer | Platform | Hamburg | |
Emma Wilson | Staff Engineer | Platform | Berlin | |
Sofia Martinez | Engineering Manager | Platform | Munich | |
Noah Kim | DevOps Engineer | Platform | Hamburg | |
Ethan Müller | QA Engineer | Platform | Hamburg | |
Design (12 items) | ||||
Liam Chen | Product Designer | Design | Hamburg | |
Olivia Brown | UX Researcher | Design | Munich | |
Isabella Singh | Design Lead | Design | Berlin | |
Liam Chen | Product Designer | Design | Hamburg | |
Olivia Brown | UX Researcher | Design | Munich | |
Isabella Singh | Design Lead | Design | Berlin | |
Liam Chen | Product Designer | Design | Hamburg | |
Olivia Brown | UX Researcher | Design | Munich | |
Isabella Singh | Design Lead | Design | Berlin | |
Liam Chen | Product Designer | Design | Hamburg | |
Olivia Brown | UX Researcher | Design | Munich | |
Isabella Singh | Design Lead | Design | Berlin | |
Product (12 items) | ||||
James Park | Frontend Developer | Product | Remote | |
Lucas Weber | Backend Developer | Product | Berlin | |
Mia Zhang | Product Manager | Product | Remote | |
James Park | Frontend Developer | Product | Remote | |
Lucas Weber | Backend Developer | Product | Berlin | |
Mia Zhang | Product Manager | Product | Remote | |
James Park | Frontend Developer | Product | Remote | |
Lucas Weber | Backend Developer | Product | Berlin | |
Mia Zhang | Product Manager | Product | Remote | |
James Park | Frontend Developer | Product | Remote | |
Lucas Weber | Backend Developer | Product | Berlin | |
Mia Zhang | Product Manager | Product | Remote | |
Data (8 items) | ||||
Aisha Patel | Data Scientist | Data | Berlin | |
Alexander Novak | ML Engineer | Data | Munich | |
Aisha Patel | Data Scientist | Data | Berlin | |
Alexander Novak | ML Engineer | Data | Munich | |
Aisha Patel | Data Scientist | Data | Berlin | |
Alexander Novak | ML Engineer | Data | Munich | |
Aisha Patel | Data Scientist | Data | Berlin | |
Alexander Novak | ML Engineer | Data | Munich | |
Platform (16 items)
Design (12 items)
Product (12 items)
Data (8 items)
Modes
sticky takes boolean | 'toolbar' | 'header' | 'both'. Which layers
that pins:
| Value | Toolbar | Column header | Group header |
|---|---|---|---|
false (default) | scrolls | scrolls | scrolls |
sticky / "both" | pins | pins | pins |
"toolbar" | pins | scrolls | scrolls |
"header" | scrolls | pins | pins |
"header" takes the group header with it: it is the section
marker of the same header, and a group whose name scrolls away tells the reader nothing.
sticky on its own
Name | Role | Department | Location |
|---|---|---|---|
Emma Wilson | Staff Engineer | Platform | Berlin |
Liam Chen | Product Designer | Design | Hamburg |
Sofia Martinez | Engineering Manager | Platform | Munich |
James Park | Frontend Developer | Product | Remote |
Aisha Patel | Data Scientist | Data | Berlin |
Noah Kim | DevOps Engineer | Platform | Hamburg |
<Table
{items}
{columns}
sticky
/>With stickyOffset for app-shell top bar
Name | Role | Department | Location |
|---|---|---|---|
Emma Wilson | Staff Engineer | Platform | Berlin |
Liam Chen | Product Designer | Design | Hamburg |
Sofia Martinez | Engineering Manager | Platform | Munich |
James Park | Frontend Developer | Product | Remote |
Aisha Patel | Data Scientist | Data | Berlin |
Noah Kim | DevOps Engineer | Platform | Hamburg |
<Table
{items}
{columns}
sticky="header"
stickyOffset={64}
/>Custom toolbar
The default toolbar is the SmartFilterBar. Override
it with the toolbar snippet; the custom content inherits
the same sticky wrapper.
Custom toolbar snippet
<Table {items} {columns} sticky enableSmartFilter={false}>
{#snippet toolbar()}
<div class="flex items-center justify-between p-3">
<h3 class="text-base font-semibold">Active employees</h3>
<button class="...">Export CSV</button>
</div>
{/snippet}
</Table>Contained scroll — fit="viewport"
Page-relative sticky pinning and in-table horizontal
scroll are mutually exclusive: a single element cannot be both a sticky-pin host and a
scroll ancestor. So with sticky="header", a table
wider than the viewport pushes its horizontal overflow onto the page, and the whole
layout scrolls sideways.
fit picks between the two models. "content", the default, is the page-relative one
above. "viewport" makes the table its own scroll
container: it is height-capped to the viewport (measured for you, with no max-height of your own), the column and group headers
pin to the top of the box, a total summary row pins to its bottom, and the toolbar
and pagination stay fixed outside the scrolling area. Only the rows scroll, horizontally and
vertically.
A total summary row pins to the bottom edge of whichever scroll box the table owns — this
one, or a virtualized table's virtualHeight box. A page-relative table has no bottom
edge of its own, so its summary stays in the flow behind the last row. Group summary rows belong
to their group and never pin.
Full-height list page
Scroll the rows inside the frame — the toolbar, the column header and the group header
stay put, and the page around the frame never moves. Switch the frame to fit="content" to feel the difference, or open the demo full-screen.
<Table
{items}
{columns}
fit="viewport"
/>fit="viewport" supersedes sticky and stickyOffset (the measured top absorbs app-shell
offsets), and it has no effect on a virtualized table, which already has a scroll box of
its own — virtualHeight — and pins its header and its summary
against that one.
Caveats
- Sticky pinning anchors to the nearest scrollable ancestor, so wrapping the table in a
container with
overflow: auto/hiddenbinds the pin to that container. Inside a Drawer body that is what you want. Inside an accidentaloverflowwrapper it is why the header stops pinning to the page. - Enabling
sticky="header"or"both"disables the table's internal horizontal scrolling, since the scroll area cannot be both a sticky pin host and a scroll ancestor. Very wide tables fall back to page-level horizontal scrolling. Switch tofit="viewport"to contain it instead. unstyledmode strips the sticky classes, because pinning is a layout function rather than pure styling. Put them back onslotClasses.toolbar,slotClasses.theadandslotClasses.groupHeader, using the offsets the table publishes on its container:--blocks-table-sticky-top(yourstickyOffset),--blocks-table-toolbar-hand--blocks-table-thead-h. Each layer'stopis the sum of the ones above it. Keep thestickyprop set while you do it:unstyledtakes away the classes, not the behaviour, and the prop is what attaches the two height measurements. Drop it and both custom properties stay unwritten, so every layer pins at the same offset and the group header lands on top of the column header.- A
fit="viewport"box reaches the bottom of the viewport, so it assumes nothing sits below it. Bottom padding on an ancestor, or a following sibling, pushes the page past100dvhand you get a second scrollbar next to the table's own. The container reports its resolved mode asdata-fit="viewport"ordata-fit="content", so a layout can drop that inset wherever the cap applies — which is every width:main:has([data-fit='viewport']) { padding-block-end: 0 }.