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
- Under
columnResize, awidthin the column def is an initial width: once the user drags, their override wins for the rest of the session. Without the prop it is simply the width. - To persist what the user chose, read
api.getColumnWidths()and replay it withapi.setColumnWidth(id, w)- see Programmatic resizing above.api.getState()carriescolumnWidthstoo, if you are already saving a whole view.
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
- Column Resizing and Reordering - Let Users Shape the Grid - How to wire drag-to-resize and drag-to-reorder in SvGrid, then persist the layout to localStorage so it survives page reloads.
- Multi-Level (Grouped) Column Headers in SvGrid - Band related columns under a shared parent header using SvGrid's nested column definition - how to nest, pin, combine with sorting and filtering, and when NOT to use grouping.
- A Custom Column Header Menu in SvGrid - Build a per-column header menu for sort, hide, pin, and custom actions using header snippets and your own dropdown component.