Row height

Row height is a single integer in pixels.

Open the live example: Custom cells + themes (Rows & Cells)

<SvGrid {data} {columns} features={{}} rowHeight={36} />

The default is 30 px. The virtualizer reads rowHeight and uses it to compute the visible window and total scroll height.

Because the virtualizer needs the height as a number up front, row height is a prop rather than a CSS token, and the grid writes it as an inline style on each row. A stylesheet rule cannot set it.

Density

A density toggle is just a derived rowHeight:

<script lang="ts">
  let density = $state<'compact' | 'normal' | 'comfortable'>('normal')
  const px = $derived(density === 'compact' ? 28 : density === 'comfortable' ? 48 : 30)
</script>

<SvGrid {data} {columns} features={{}} rowHeight={px} />

The example gallery's demos/10-custom-cells-and-themes.svelte shows the density toggle in full.

Auto row height (size each row to its content)

autoRowHeight lets cell text wrap and sizes every row to its tallest cell:

<SvGrid {data} {columns} autoRowHeight />

Rows are measured after they render, so this works with virtualization. Before a row has been measured the grid uses rowHeight (or 30) as its estimate, which keeps the scrollbar stable as you scroll into rows for the first time:

<!-- 44px is the starting guess; each row settles to its real height -->
<SvGrid {data} {columns} autoRowHeight rowHeight={44} />

Things worth knowing:

Variable row height (you supply the numbers)

Pass a function to size rows yourself, without measuring:

<SvGrid {data} {columns} rowHeight={(i) => (data[i].tall ? 80 : 36)} />

The virtualizer handles the variable-size case natively (cumulative offsets), so scrolling and the total height stay correct. The same engine is available headless if you are building your own row layout:

import { createSvelteVirtualizer } from '@svgrid/grid'

const virtualizer = createSvelteVirtualizer({
  count: rows.length,
  estimateSize: (index) => rows[index]!.tall ? 80 : 36,
  viewportHeight: scrollEl.clientHeight,
  overscan: 6,
})

// It owns no DOM. Feed it your scroller's numbers as they change:
virtualizer.setScrollOffset(scrollEl.scrollTop)
virtualizer.setViewportHeight(scrollEl.clientHeight)

See packages/grid/src/virtualization/.

Letting the user drag it (rowResize)

The three options above all decide the height for the user. rowResize hands it to them: drag a row's bottom edge, or focus the grip and use Up/Down (Shift for 1px steps). It is off by default, because it puts a drag target on every row and most grids do not want one.

The grid stores the dragged heights itself, keyed by row index, so this works as a bare boolean - you do not need a function-valued rowHeight as well:

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

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

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace', note: 'Drag the bottom edge of this row.' },
    { id: 2, name: 'Grace Hopper', note: 'Or focus the grip and press Down.' },
    { id: 3, name: 'Linus Torvalds', note: 'Each row keeps its own height.' },
  ]

  const columns: GridColumns<Person> = [
    { field: 'name', header: 'Name', width: 170 },
    { field: 'note', header: 'Note', width: 320 },
  ]
</script>

<SvGrid data={people} {columns} rowResize containerHeight={200} />

Note that the grid above never asks for showRowNumbers. Switching on rowResize brings the row header column with it, because that is where the grip belongs - a drag target on the edge of a data cell works, but reads as an accident.

You can still override it. An explicit showRowNumbers={false} wins, and the grip falls back to the row's first cell; a column of your own carrying cellClass: 'sv-row-gutter' is used ahead of either.

Two things to know:

Header height

Header height is independent of row height. See Column headers for how to size it.

Row-number column width

When showRowNumbers={true}, the leading row-number column defaults to 56 px, which fits up to 99,999. For larger datasets, bump the width via rowNumberWidth:

<!-- One million rows: "1,000,000" needs ~ 92 px to stay fully visible -->
<SvGrid
  {data}
  {columns}
  features={{}}
  showRowNumbers={true}
  rowNumberWidth={92}
  rowHeight={18}
  virtualization={true}
  containerHeight="100%"
/>

Rule of thumb: budget ~ 8 px per digit plus 14 px of padding. So:

Row count Largest number Suggested rowNumberWidth
< 1 000 "999" 40
< 100 000 "99,999" 56 (default)
< 10 000 000 "9,999,999" 92

Demo 78 ("1 million rows") uses 92 px so the millionth row's index stays legible at the bottom of the scroll.

One height for every row

rowHeight is the density control: a fixed number the virtualizer can rely on. Because every row is the same height, the grid can work out what is on screen with arithmetic rather than measurement, which is why this is the fast path.

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

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

  const seed: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   salary: 142000,
      bio: 'Wrote the first algorithm intended for a machine, and the first account of what a general-purpose computer could do beyond arithmetic.' },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', salary: 168000,
      bio: 'Built the first compiler and argued that programs should be written in something closer to English than to machine code.' },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', salary: 155000,
      bio: 'Wrote a kernel and, later, the version control system that most of the industry now runs on.' },
  ]

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

<SvGrid data={seed} {columns} rowHeight={28} />

<SvGrid data={seed} {columns} rowHeight={52} />

Letting the content decide

autoRowHeight sizes each row to its own content, so long text wraps instead of being clipped. It costs a measurement pass per row - worth it for prose, not worth it for a hundred thousand numbers.

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

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

  const seed: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   salary: 142000,
      bio: 'Wrote the first algorithm intended for a machine, and the first account of what a general-purpose computer could do beyond arithmetic.' },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', salary: 168000,
      bio: 'Built the first compiler and argued that programs should be written in something closer to English than to machine code.' },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', salary: 155000,
      bio: 'Wrote a kernel and, later, the version control system that most of the industry now runs on.' },
  ]

  const columns: GridColumns<Person> = [
    { field: 'name', header: 'Name', width: 170 },
    { field: 'bio',  header: 'Bio',  width: 380 },
  ]
</script>

<SvGrid data={seed} {columns} autoRowHeight />

See also

Live examples

  • Custom cells + themes - Avatars, sparklines, progress bars, density toggle, dark mode, full a11y.
  • 1 million rows - A literal 1,000,000-row dataset with sort, filter, group, scroll, and inline edit all on. Chunked generation with progress.

Related articles