Vanilla JS Grid Column Pinning: Freeze Left and Right Columns in TypeScript (2026)

Vanilla TSTutorialColumn Pinning

Pin vanilla TypeScript table columns to the left or right with sticky headers—strict TypeScript examples for simple-table-core and a comparison to Tabulator and Handsontable.

For Vanilla JS / TypeScript developers building data grids in 2026.

Wide tables with 20+ columns become unusable without pinning. Users lose context as they scroll horizontally—what row am I on? Pin the identifier column on the left and an actions column on the right and the UX clicks back into place.

This tutorial walks through column pinning patterns for the vanilla JS / TS data grid landscape and shows the simple-table-core setup with strict TypeScript.

If you also need virtualization, grouping with aggregations, and inline editing alongside pinning, simple-table-core is the focused MIT pick—~70 kB gzipped, framework-agnostic.

Why it matters

Context anchoring

Users don't lose track of what row they're on as they scroll horizontally.

Action accessibility

Pin Edit / Delete / Open buttons on the right so they're always within reach.

Wide-table support

30+ columns become navigable when key columns stay sticky.

Excel-like ergonomics

Power users expect Freeze Panes; pinning delivers the same affordance.

Vanilla JS / TypeScript library comparison

LibrarySupportNotes
simple-table-coreBuilt-in (left + right)pinned: 'left' | 'right' on HeaderObject; sticky on horizontal scroll.
TabulatorBuilt-infrozen: true on column defs; sticky to left or right via column position.
Grid.jsManualNo native pinning—use position: sticky CSS on column cells.
HandsontableBuilt-in (commercial)Pinning built-in but commercial license required.
jSpreadsheetBuilt-infreezeColumns option; spreadsheet-style.

Implementation: simple-table-core

Set pinned: 'left' or pinned: 'right' on individual HeaderObjects. simple-table-core handles z-index, sticky positioning, and shadow indicators automatically.

Keep pinned columns narrow (under ~30% of viewport) so the scrolling area stays usable. Combine with columnResizing if users should be able to resize pinned columns.

Common pitfalls

Too many pinned columns

Problem: Users pin 8 of 12 columns; the scrolling area becomes useless.

Solution: Cap pinned columns at 2-3 each side, or warn the user beyond a threshold.

Pinned column width mismatch

Problem: Resizing a pinned column doesn't update the sticky offset.

Solution: Pick a library that handles offset recalculation on resize. simple-table-core does this automatically.

Z-index battles in shadow DOM

Problem: Editors / popovers render below the pinned column inside a shadow root.

Solution: Render popovers into the document body via slot-based portals, or use a high z-index inside the shadow tree.

Mobile horizontal scroll feels broken

Problem: Pinned columns over-fill the viewport on small screens.

Solution: Use a window matchMedia listener and call setHeaders to clear pinning at < 768px.

Frequently asked questions

Can users reorder pinned columns?
Yes—simple-table-core supports column reordering, including across the pinned/unpinned boundary. Set columnReordering: true.
Does it work in a web component?
Yes. Mount inside the shadow root: pass shadowRoot.querySelector('#host') as the element.
Does pinning work with virtualization?
Yes. The pinned columns are rendered separately from the virtualized scroll area; performance is unchanged for 1M rows.

Wrap-up

Column pinning in vanilla JS / TS is a single property on simple-table-core. Tabulator supports it natively too; Grid.js requires DIY sticky CSS; Handsontable is commercial.

Cap the number of pinned columns and disable pinning on small viewports to keep the scrolling area usable.

Add column pinning to your vanilla TS grid

simple-table-core ships left/right pinning, virtualization, and grouping in one MIT package—~70 kB gzipped, strict TypeScript, ESM-first.