Column pinning + freezing
Wide 13-column grid. Pin Company left and Price right via the column menu; the middle scrolls under sticky edges.
A live, editable Svelte 5 data grid example from the SvGrid gallery (Columns). See the SvGrid documentation for the full API.
About this example
Column pinning in the Svelte 5 data grid. A wide eight-column grid pins Company to the left and Unit price to the right through initialColumnPinning, so they stay sticky while the middle columns scroll under them; the header menu offers Pin to left, Pin to right and Unpin, and api.setColumnPinning does the same from code. Pinning works with column virtualization on, so a wide grid still renders only the columns in view plus the pinned ones.
Wide grid (8 generous columns ~2050px total) so horizontal scrolling kicks in on any reasonable viewport. Company is pre-pinned to the LEFT and Unit price to the RIGHT - they stay sticky while the middle columns slide under them when you scroll.
Try it:
- Scroll the grid horizontally - Company stays anchored on the left, Unit price on the right, the rest scroll between them.
- Hover any middle-column header → click the ⋮ that appears → "Pin to left" / "Pin to right" / "Unpin column".
Pinning works with column virtualization on (the default): the pinned columns render on their own either side of the virtual window, so only the columns in view plus the pinned ones are in the DOM.
A programmatic api.setColumnPinning(id, side) is on the v1.x roadmap; today the initialColumnPinning prop covers the on-mount case and the column menu covers user-driven re-pinning.
Imports, features and API used
Imports: @svgrid/grid, ../shared/seed
Table features registered: rowSortingFeature, columnFilteringFeature
Columns: company (Company), product (Product), orderId (Order ID), country (Country), sellDate (Sell date), quantity (Quantity), inStock (In stock), price (Unit price)
SvGridApi methods called: api.setColumnPinning()
Frequently asked questions
How do I pin a column on mount?
Pass initialColumnPinning={{ left: ['company'], right: ['price'] }} to <SvGrid>. Users can change it later from each header's menu, and api.setColumnPinning({ left, right }) sets it from code.
Does pinning work with column virtualization?
Yes. Pinned cells use position: sticky, so they have to stay in the DOM; the grid renders the pinned-left columns, the virtual window and the pinned-right columns as separate runs, with a spacer cell for the columns between them. A 1,000-column grid with both edges pinned renders about twenty cells per row, not a thousand.
Can pinned columns be resized?
Yes. columnResize is on in this demo and works on pinned columns like any other; the pinned edge keeps its position while the column grows.
Related documentation
Related articles
- Pinned (Frozen) Columns for Wide Svelte Data Grids - How to pin identity and action columns in SvGrid so users never lose their place while scrolling wide tables - declarative config, runtime API, and the tradeoffs that matter.
Source code (25-column-pinning.svelte)
<!-- Documented in: docs/help/recipes.md -->
<script lang="ts">
/**
* 25. Column pinning + freezing
* -----------------------------
* Wide grid (8 generous columns ~2050px total) so horizontal scrolling
* kicks in on any reasonable viewport. Company is pre-pinned to the
* LEFT and Unit price to the RIGHT - they stay sticky while the middle
* columns slide under them when you scroll.
*
* Try it:
* - Scroll the grid horizontally - Company stays anchored on the left,
* Unit price on the right, the rest scroll between them.
* - Hover any middle-column header → click the ⋮ that appears →
* "Pin to left" / "Pin to right" / "Unpin column".
*
* Pinning works with column virtualization on (the default): the pinned
* columns render on their own either side of the virtual window, so only
* the columns in view plus the pinned ones are in the DOM.
*
* A programmatic `api.setColumnPinning(id, side)` is on the v1.x
* roadmap; today the `initialColumnPinning` prop covers the on-mount
* case and the column menu covers user-driven re-pinning.
*/
import {
SvGrid,
tableFeatures,
rowSortingFeature,
columnFilteringFeature,
type GridColumns,
} from '@svgrid/grid'
import { makeOrders, type Order } from '../shared/seed'
const features = tableFeatures({
rowSortingFeature,
columnFilteringFeature,
})
let rows = $state<Order[]>(makeOrders(500))
// 8 generous columns - total ~2050px so the grid overflows horizontally
// on any normal monitor. Pinning is only meaningful when the grid
// actually scrolls; the demo has to force that condition to demonstrate
// the feature.
const columns: GridColumns<Order> = [
{ field: 'company', header: 'Company', width: 240 },
{ field: 'product', header: 'Product', width: 320 },
{ field: 'orderId', header: 'Order ID', width: 180 },
{ field: 'country', header: 'Country', width: 200 },
{ field: 'sellDate', header: 'Sell date', width: 220,
format: { type: 'date', pattern: 'y-m-d' } },
{ field: 'quantity', header: 'Quantity', width: 170,
format: { type: 'number', options: { maximumFractionDigits: 0 } } },
{ field: 'inStock', header: 'In stock', width: 160 },
{ field: 'price', header: 'Unit price', width: 200,
format: { type: 'currency', currency: 'USD' } },
]
</script>
<section class="flex flex-col flex-1 min-h-0 gap-3">
<div
class="shrink-0 rounded-lg border px-4 py-3"
style="border-color: var(--sg-border); background: var(--sg-header-bg);"
>
<p class="text-sm font-semibold" style="color: var(--sg-fg);">
Company is pinned LEFT · Unit price is pinned RIGHT
</p>
<ol class="mt-1 text-xs list-decimal pl-5 space-y-0.5" style="color: var(--sg-fg);">
<li>
<strong>Scroll horizontally</strong> - Company and Unit price stay anchored at the edges
while everything else slides between them.
</li>
<li>
<strong>Pin another column</strong> - hover any middle header → click the
<code style="color: var(--site-accent-2, #22d3ee);">⋮</code> → pick
<em>Pin to left</em>, <em>Pin to right</em>, or <em>Unpin column</em>.
</li>
</ol>
</div>
<div class="flex-1 min-h-0 pinning-host">
<SvGrid responsive={true}
columnResize
data={rows}
columns={columns}
features={features}
filterMode="menu"
showRowNumbers={true}
showPagination={true}
pageSize={50}
enableCellSelection={true}
rowHeight={36}
containerHeight="100%"
fitColumns={false}
initialColumnPinning={{ left: ['company'], right: ['price'] }}
/>
</div>
<footer class="pin-foot text-xs shrink-0">
{rows.length} rows · {columns.length} columns ·
<code>fitColumns={false}</code> +
<code>columnVirtualization={false}</code>
so the grid keeps its declared widths and the sticky pinned columns work.
</footer>
</section>
<style>
.pin-foot { color: var(--sg-muted, #64748b); }
/* Punch up the visual contrast for pinned columns so the user can
immediately see which columns are frozen. The grid already supports
these tokens; this demo just turns the dial higher to demo the
feature. */
.pinning-host {
/* Body cells: a stronger accent-tinted background than the default
8% mix - the pinned strip should read as a clearly different surface.
The two color-mix percentages MUST sum to 100% - otherwise CSS scales
the result's alpha down (e.g. 55% + 18% = 73% -> 0.73 alpha), which
makes the pinned column semi-transparent and lets the scrolling
middle columns bleed through. */
--sg-pinned-bg: color-mix(in oklab, var(--sg-bg, #ffffff) 82%, var(--sg-accent, #2563eb) 18%);
/* Header cells: even bolder so the frozen header is unmistakable. */
--sg-pinned-header-bg: color-mix(in oklab, var(--sg-header-bg, #f1f5f9) 70%, var(--sg-accent, #2563eb) 30%);
/* Divider line on the inside edge - solid accent so it reads as
intentional, not as a normal cell border. */
--sg-pinned-divider: var(--sg-accent, #2563eb);
}
:global(html[data-theme='dark']) .pinning-host {
--sg-pinned-bg: color-mix(in oklab, var(--sg-bg, #181d27) 76%, var(--sg-accent, #3b82f6) 24%);
--sg-pinned-header-bg: color-mix(in oklab, var(--sg-header-bg, #1e2433) 64%, var(--sg-accent, #3b82f6) 36%);
--sg-pinned-divider: var(--sg-accent, #3b82f6);
}
/* No icon - the accent-tinted background tokens above (--sg-pinned-bg
/ --sg-pinned-header-bg) carry the "this column is frozen" signal
on their own. Punch the header tint further to make it unmistakable
even at a glance. */
.pinning-host {
--sg-pinned-header-bg: color-mix(in oklab, var(--sg-header-bg, #f1f5f9) 58%, var(--sg-accent, #2563eb) 42%);
}
:global(html[data-theme='dark']) .pinning-host {
--sg-pinned-header-bg: color-mix(in oklab, var(--sg-header-bg, #1e2433) 52%, var(--sg-accent, #3b82f6) 48%);
}
/* Make the pinned header text white on the now-strong accent fill so
contrast stays readable in both modes. */
.pinning-host :global(.sv-grid-head .sv-grid-column[data-pinned]) {
color: #fff;
}
.pinning-host :global(.sv-grid-head .sv-grid-column[data-pinned] *) {
color: inherit;
}
</style>More Columns examples
- Columns hierarchy + manager - Side-panel tree of grouped columns: drag leaves to reorder, click a chevron to collapse a group into one summary column, toggle visibility per leaf or whole group.
- Column layout API - setColumnWidth + setColumnPinning + getColumnWidths + getColumnPinning. Save the snapshot to localStorage, restore on reload, drive widths and pins from buttons.
- Tool panel (Columns + Filters) - The docked enterprise sidebar, two tabs. Columns: toggle visibility, reorder up/down, group by a column. Filters: an operator + value control per column (numeric operators come free via cellDataType), kept in sync with the column menu. Enable with the toolPanel prop.
- Column reorder - Set enableColumnReorder on <SvGrid> and every header becomes draggable, with a drop indicator. api.setColumnOrder / getColumnOrder + onColumnOrderChange event; persist to restore across reloads.
- Autosize columns - api.autosizeColumn(id) and api.autosizeAllColumns() snap columns to the widest visible cell via canvas-based text measurement. The column header menu has an "Autosize" item that calls the same code. Manual drag-resize still works.