Column sizing

Each column has a pixel width. The default for all columns is the grid's columnWidth prop (default ~140 px); each column can override via its width field.

Open the live example: Column layout API (Columns)

The examples on this page run against these rows:

<script lang="ts">
  import { SvGrid, type GridColumns } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    department: string
    city: string
    age: number
    salary: number
  }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   age: 36, salary: 142000 },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', age: 45, salary: 168000 },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', age: 54, salary: 155000 },
    { id: 4, name: 'Radia Perlman',  department: 'Networking',  city: 'Seattle',  age: 49, salary: 161000 },
    { id: 5, name: 'Barbara Liskov', department: 'Platform',    city: 'Boston',   age: 52, salary: 172000 },
  ]

  let rows = $state<Person[]>(people)
  const data = people

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 200 },
    { field: 'department', header: 'Department', width: 150 },
    { field: 'city',       header: 'City',       width: 140 },
    { field: 'age',        header: 'Age',        width: 90 },
    { field: 'salary',     header: 'Salary',     width: 130, format: { type: 'currency', currency: 'USD' } },
  ]
</script>
const columns: GridColumns<Person> = [
  { field: 'firstName',  header: 'First name', width: 150 },
  { field: 'department', header: 'Department', width: 180 },
  { field: 'salary',     header: 'Salary',     width: 120 },
]
<SvGrid {columns} {data} features={{}} columnWidth={140} />

User resizing

Off by default. Set columnResize and every header grows a drag handle on its right edge: drag to widen or narrow, minimum 40 px, or focus the handle and use Left/Right (Shift for 1px steps). Widths are stored per column id inside the grid, so a resize survives sorting and filtering.

Leaving it off is the reason width means what it says - the width you asked for, not a starting point a user can drag away:

<script lang="ts">
  import { SvGrid, type GridColumns } from '@svgrid/grid'

  type Person = { id: number; name: string; city: string }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace', city: 'London' },
    { id: 2, name: 'Grace Hopper', city: 'New York' },
  ]

  const columns: GridColumns<Person> = [
    { field: 'name', header: 'Name', width: 180 },
    { field: 'city', header: 'City', width: 150 },
  ]
</script>

<p>Fixed widths (the default):</p>
<SvGrid data={people} {columns} containerHeight={140} />

<p>With <code>columnResize</code> - drag a header edge:</p>
<SvGrid data={people} {columns} containerHeight={140} columnResize />

A drag is one undo

A width the user drags, or double-clicks into, is one step in the grid's undo history: Ctrl+Z puts it back, Ctrl+Y widens it again. onColumnResize reports each such change with the column id and the new width. api.setColumnWidth and api.autosizeColumn record nothing and fire nothing, so restoring saved widths does not fill the history.

Locking one column: resizable: false

columnResize turns resizing on for the grid; a column opts back out with resizable: false. Use it where the width is part of the layout rather than a preference - a row-number gutter, a checkbox column, a fixed icon column.

Opting out removes that column's handle entirely, so the drag, the arrow keys and double-click-to-autosize all go with it, and the column menu drops its Autosize item. Nothing is left that lets a user change the width by accident:

<script lang="ts">
  import { SvGrid, type GridColumns } from '@svgrid/grid'

  type Person = { id: number; name: string; city: string }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace', city: 'London' },
    { id: 2, name: 'Grace Hopper', city: 'New York' },
  ]

  const columns: GridColumns<Person> = [
    // Fixed: the row id is 60px because that is what fits, not a preference.
    { field: 'id',   header: '#',    width: 60, resizable: false },
    { field: 'name', header: 'Name', width: 200 },
    { field: 'city', header: 'City', width: 160 },
  ]
</script>

<SvGrid data={people} {columns} columnResize containerHeight={150} />

The column option only ever subtracts. resizable: true on a grid without columnResize does nothing - the grid-wide prop is still the switch.

Programmatic sizing is unaffected either way: api.setColumnWidth(), api.autosizeColumn() and fitColumns all still apply to a locked column, exactly as they do when columnResize is off.

The handles come from an action the grid loads on demand, so a grid that leaves columnResize off never downloads that code.

Programmatic resizing + persistence

The imperative API exposes setColumnWidth and getColumnWidths:

<script lang="ts">
  import type { SvGridApi } from '@svgrid/grid'
  let api = $state<SvGridApi<typeof features, Person> | null>(null)

  function save() {
    if (!api) return
    localStorage.setItem('widths', JSON.stringify(api.getColumnWidths()))
  }
  function restore() {
    if (!api) return
    const saved: Record<string, number> = JSON.parse(localStorage.getItem('widths') ?? '{}')
    for (const [id, w] of Object.entries(saved)) api.setColumnWidth(id, w)
  }
</script>

<SvGrid {data} {columns} features={features}
  onApiReady={(next) => (api = next)} />

<button onclick={save}>Save layout</button>
<button onclick={restore}>Restore layout</button>

getColumnWidths() returns every column's current effective width (user resize OR columnDef width OR grid-wide default), so the snapshot round-trips cleanly.

Auto-fit

fitColumns={true} scales every column proportionally to fill the viewport - the wrapper handles rounding-residue absorption + a modest shrink-to-fit (down to ~85 % of natural widths). For "fit to content" (longest cell text), pre-compute per-column widths once after a data load and call api.setColumnWidth(id, w) per column.

Column virtualization

With many columns, enable column virtualization so only the visible columns render:

<SvGrid
  {columns}
  {data}
  features={{}}
  columnVirtualization={true}
  columnWidth={120}
  columnOverscan={3}
/>

columnOverscan is how many columns stay rendered ahead of a horizontal scroll at all times (default 3), with one kept behind it. The rendered columns don't move on every column boundary: they stay put while that many are left ahead, then move three at once. Each move updates every rendered row, whether one column comes in or four, so fewer, larger moves are less work, and the scroll frames in between change nothing. Right after a move, up to columnOverscan + 2 columns are rendered ahead. Raise it if fast scrolling shows blank columns before they fill in; lower it for less work per scroll.

See examples/src/demos/06-large-dataset.svelte for a 100-column virtualized grid.

Collapsing a column to nothing

api.setColumnCollapsed(id, true) folds a column away the way a spreadsheet hides one: the column keeps its place, its cells and the width it had, and takes no room until setColumnCollapsed(id, false) unfolds it. Its neighbours' borders meet at the seam. Arrow keys, Tab and Enter step over a collapsed column, so the cursor never rests on it; api.setActiveCell and a sheet's Name Box still can. api.isColumnCollapsed(id) reads the state.

This is not setColumnVisible(id, false), which takes the column out of the model: every column after a hidden one moves up an index, which is right for a "Choose columns" panel and wrong for anything that addresses cells by position, such as a sheet's formulas and formats. Collapse when the indices must hold; hide when the column should be gone.

api.setColumnCollapsed('cost', true)   // folded: cells and width kept, no room
api.isColumnCollapsed('cost')          // true
api.setColumnCollapsed('cost', false)  // back at the width it had

The spreadsheet shell's Hide and Unhide (Ctrl+9, Ctrl+0, and the header menus) are this, kept per sheet.

Gotchas

Try it

Without fitColumns the explicit widths are used as-is and any leftover space stays empty on the right. With it, the columns scale to fill. Both carry columnResize, so drag a header edge in either - a user resize wins from then on, including over fitColumns.

<SvGrid data={people} {columns} columnResize />

<SvGrid data={people} {columns} columnResize fitColumns />

See also

Live examples

  • Column layout API - setColumnWidth + setColumnPinning + getColumnWidths + getColumnPinning. Save the snapshot to localStorage, restore on reload, drive widths and pins from buttons.
  • 100k rows × 100 columns - Row + column virtualization. Chunked load with progress + cancellation.

Related articles