Drag to pin columns

Drag the line at the edge of the pinned columns to pin more or fewer: pinSplitter snaps to column edges, shows how many it will pin, keeps the columns where they are on screen and leaves room for the ones that scroll. Escape cancels; Tab to it and use the arrow keys. Free, and loaded only when the prop is on.

A live, editable Svelte 5 data grid example from the SvGrid gallery (Columns). See the SvGrid documentation for the full API.

About this example

A Svelte 5 data grid where the user pins columns by dragging: a divider at the edge of the pinned columns snaps to column edges as it moves, shows how many columns it will pin, and keeps every column where it is on screen when it changes sides. A 14-column order book starts with the Order column pinned; drag right to pin Customer and Region beside it, left to let them scroll.

pinSplitter draws a divider at the right edge of the left-pinned columns (the row numbers when none are pinned). Drag it right and the columns it passes are pinned; drag it left and they scroll again. It snaps to column edges, shows how many columns it will pin, keeps 120px for the columns that scroll, and never pins them all. A column that changes sides stays where it is on screen. Escape drops a drag; with the keyboard, Tab to the divider and use the arrow keys (Home unpins all).

Free in @svgrid/grid, and loaded only when the prop is on. Its words come from localization.text like the rest of the grid's UI.

Imports, features and API used

Imports: @svgrid/grid

Columns: order (Order), customer (Customer), region (Region), rep (Sales rep), product (Product), qty (Qty), unitPrice (Unit price), discount (Discount), total (Total), status (Status), ordered (Ordered), shipped (Shipped)

SvGridApi methods called: api.getColumnPinning(), api.setColumnPinning()

Frequently asked questions

Is the pin splitter part of the free grid?

Yes. pinSplitter is a prop of the MIT @svgrid/grid package. The grid loads its code only when the prop is on, so a grid without it does not ship it.

Can the user pin every column?

No. The divider leaves 120px for the columns that scroll and stops one column short of the last, so there is always something to scroll.

Does it work from the keyboard?

Yes. The divider is a focusable separator: the arrow keys pin or unpin one column at a time and Home unpins them all. Screen readers hear how many columns are pinned.

Can I translate its labels?

Yes, through localization.text: pinSplitterLabel, pinSplitterHint, pinSplitterNone, pinSplitterOne and pinSplitterMany ({count} is the number of pinned columns).

Related documentation

Related articles

Source code (510-column-pin-splitter.svelte)

<script lang="ts">
  /**
   * 510. Drag to pin columns
   * ------------------------
   * `pinSplitter` draws a divider at the right edge of the left-pinned
   * columns (the row numbers when none are pinned). Drag it right and the
   * columns it passes are pinned; drag it left and they scroll again. It
   * snaps to column edges, shows how many columns it will pin, keeps 120px
   * for the columns that scroll, and never pins them all. A column that
   * changes sides stays where it is on screen. Escape drops a drag; with the
   * keyboard, Tab to the divider and use the arrow keys (Home unpins all).
   *
   * Free in @svgrid/grid, and loaded only when the prop is on. Its words
   * come from `localization.text` like the rest of the grid's UI.
   */
  import { SvGrid, type GridColumns, type SvGridApi } from '@svgrid/grid'

  type Order = {
    id: number
    order: string
    customer: string
    region: string
    rep: string
    product: string
    qty: number
    unitPrice: number
    discount: number
    total: number
    status: string
    ordered: string
    shipped: string
    carrier: string
  }

  const CUSTOMERS = ['Northwind Traders', 'Contoso Ltd', 'Fabrikam Inc', 'Adventure Works', 'Tailspin Toys', 'Wide World Importers', 'Litware', 'Proseware']
  const REGIONS = ['North America', 'Europe', 'Asia Pacific', 'Latin America']
  const REPS = ['Ana Ruiz', 'Ben Okafor', 'Cleo Park', 'Dev Shah', 'Elena Novak']
  const PRODUCTS = ['Standing desk', 'Ergonomic chair', 'Monitor arm', 'Desk lamp', 'Cable tray', 'Footrest']
  const STATUS = ['Processing', 'Shipped', 'Delivered', 'On hold']
  const CARRIERS = ['DHL', 'UPS', 'FedEx', 'Maersk']
  const day = (n: number) => new Date(Date.UTC(2026, 8, 1 + n)).toISOString().slice(0, 10)
  const rows: Order[] = Array.from({ length: 60 }, (_, i) => {
    const qty = 1 + ((i * 7) % 24)
    const unitPrice = [649, 389, 129, 89, 45, 59][i % 6]!
    const discount = [0, 0.05, 0.1, 0, 0.15][i % 5]!
    const status = STATUS[(i * 3) % 4]!
    return {
      id: i + 1,
      order: `SO-${String(24100 + i)}`,
      customer: CUSTOMERS[i % CUSTOMERS.length]!,
      region: REGIONS[(i * 5) % REGIONS.length]!,
      rep: REPS[i % REPS.length]!,
      product: PRODUCTS[i % PRODUCTS.length]!,
      qty,
      unitPrice,
      discount,
      total: Math.round(qty * unitPrice * (1 - discount) * 100) / 100,
      status,
      ordered: day(i % 28),
      shipped: status === 'Processing' || status === 'On hold' ? '' : day((i % 28) + 2),
      carrier: CARRIERS[i % CARRIERS.length]!,
    }
  })

  const columns: GridColumns<Order> = [
    { field: 'order', header: 'Order', width: 110 },
    { field: 'customer', header: 'Customer', width: 190 },
    { field: 'region', header: 'Region', width: 140 },
    { field: 'rep', header: 'Sales rep', width: 130 },
    { field: 'product', header: 'Product', width: 160 },
    { field: 'qty', header: 'Qty', width: 80, editorType: 'number' },
    { field: 'unitPrice', header: 'Unit price', width: 110, editorType: 'number', format: { type: 'currency', currency: 'USD' } },
    { field: 'discount', header: 'Discount', width: 100, editorType: 'number', format: { type: 'percent' } },
    { field: 'total', header: 'Total', width: 120, editorType: 'number', format: { type: 'currency', currency: 'USD' } },
    { field: 'status', header: 'Status', width: 120 },
    { field: 'ordered', header: 'Ordered', width: 120, cellDataType: 'dateString' },
    { field: 'shipped', header: 'Shipped', width: 120, cellDataType: 'dateString' },
    { field: 'carrier', header: 'Carrier', width: 110 },
  ]

  let api = $state.raw<SvGridApi<any, Order> | null>(null)
  // What is pinned, as the grid has it now (the splitter, the column menu or Reset).
  const pinned = $derived(api ? api.getColumnPinning().left : [])
  const headerOf = (id: string) => columns.find((c) => c.field === id)?.header ?? id
</script>

<section class="flex flex-col gap-3 h-full min-h-0">
  <div class="flex flex-wrap items-center gap-3 shrink-0">
    <p class="ps-note text-sm m-0 flex-1 min-w-[260px]">
      Drag the line at the edge of the pinned columns to pin more or fewer. It snaps to column edges; Escape cancels; Tab to it and use the arrow keys.
    </p>
    <span class="ps-pinned text-sm" aria-live="polite">
      {pinned.length ? `Pinned: ${pinned.map(headerOf).join(', ')}` : 'Nothing pinned'}
    </span>
    <button type="button" class="ps-btn text-sm" onclick={() => api?.setColumnPinning({ left: ['order'], right: [] })}>Reset</button>
  </div>
  <div class="flex-1 min-h-0">
    <SvGrid
      data={rows}
      {columns}
      getRowId={(r) => String(r.id)}
      showRowNumbers
      showRowSelection={false}
      pinSplitter
      columnResize
      sortable
      initialColumnPinning={{ left: ['order'], right: [] }}
      containerHeight="100%"
      onApiReady={(a) => (api = a)}
    />
  </div>
</section>

<style>
  .ps-note { color: var(--sg-muted, #64748b); }
  .ps-pinned {
    padding: 3px 10px; border-radius: 999px; color: var(--sg-fg, #0f172a);
    background: color-mix(in srgb, var(--sg-fg, #0f172a) 6%, transparent);
  }
  .ps-btn {
    height: 30px; padding: 0 12px; border-radius: 8px; font: inherit; cursor: pointer;
    border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
  }
</style>

View this example on GitHub

More Columns examples

  • Column pinning + freezing - Wide 13-column grid. Pin Company left and Price right via the column menu; the middle scrolls under sticky edges.
  • 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.