Docs
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
API Reference
Table API Reference
Column API Reference
Row API Reference
Cell API Reference
Header API Reference
Features API Reference
Enterprise

Solid Example: Virtualized Columns

import {
  FlexRender,
  columnSizingFeature,
  columnVisibilityFeature,
  createSortedRowModel,
  createTable,
  rowSortingFeature,
  sortFns,
  tableFeatures,
} from '@tanstack/solid-table'
import { createVirtualizer } from '@tanstack/solid-virtual'
import { For, createSignal } from 'solid-js'
import { makeColumns, makeData } from './makeData'
import type {
  Cell,
  Header,
  HeaderGroup,
  Row,
  SolidTable,
} from '@tanstack/solid-table'
import type { VirtualItem, Virtualizer } from '@tanstack/solid-virtual'
import type { Person } from './makeData'

const _features = tableFeatures({
  columnSizingFeature,
  columnVisibilityFeature,
  rowSortingFeature,
})

function App() {
  const columns = makeColumns(1_000)
  const [data, setData] = createSignal(makeData(1_000, columns))

  const refreshData = () => setData(makeData(1_000, columns))

  const table = createTable({
    _features,
    _rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
    columns,
    get data() {
      return data()
    },
    debugTable: true,
  })

  return (
    <div class="app">
      <div>({columns.length.toLocaleString()} columns)</div>
      <div>({data().length.toLocaleString()} rows)</div>
      <button onClick={refreshData}>Refresh Data</button>
      <TableContainer table={table} />
    </div>
  )
}

// Important: Keep both virtualizers and the scroll container ref in the same component.
// The ref must be undefined when createVirtualizer runs (before JSX return),
// so that onMount can set up scroll observers after the element is in the DOM.
function TableContainer(props: {
  table: SolidTable<typeof _features, Person>
}) {
  const visibleColumns = () => props.table.getVisibleLeafColumns()
  const rows = () => props.table.getRowModel().rows

  let tableContainerRef: HTMLDivElement | undefined

  // We are using a slightly different virtualization strategy for columns (compared to virtual rows)
  // in order to support dynamic row heights.
  const columnVirtualizer = createVirtualizer<
    HTMLDivElement,
    HTMLTableCellElement
  >({
    get count() {
      return visibleColumns().length
    },
    estimateSize: (index) => visibleColumns()[index].getSize(), // estimate width of each column for accurate scrollbar dragging
    getScrollElement: () => tableContainerRef ?? null,
    horizontal: true,
    overscan: 3, // how many columns to render on each side off screen (adjust this for performance)
  })

  // dynamic row height virtualization - alternatively you could use a simpler fixed row height strategy without `measureElement`
  const rowVirtualizer = createVirtualizer<HTMLDivElement, HTMLTableRowElement>(
    {
      get count() {
        return rows().length
      },
      estimateSize: () => 33, // estimate row height for accurate scrollbar dragging
      getScrollElement: () => tableContainerRef ?? null,
      // measure dynamic row height, except in firefox because it measures table border height incorrectly
      measureElement:
        typeof window !== 'undefined' &&
        navigator.userAgent.indexOf('Firefox') === -1
          ? (element) => element.getBoundingClientRect().height
          : undefined,
      overscan: 5,
    },
  )

  // Different virtualization strategy for columns - instead of absolute and translateY,
  // we add empty columns to the left and right
  const virtualPaddingLeft = () => {
    const vcs = columnVirtualizer.getVirtualItems()
    return vcs.length ? (vcs[0]?.start ?? 0) : undefined
  }

  const virtualPaddingRight = () => {
    const vcs = columnVirtualizer.getVirtualItems()
    if (!vcs.length) return undefined
    return columnVirtualizer.getTotalSize() - (vcs[vcs.length - 1]?.end ?? 0)
  }

  return (
    <div
      class="container"
      ref={tableContainerRef}
      style={{
        overflow: 'auto',
        position: 'relative',
        height: '800px',
      }}
    >
      {/* Even though we're still using semantic table tags, we must use CSS grid and flexbox for dynamic row heights */}
      <table style={{ display: 'grid' }}>
        <TableHead
          columnVirtualizer={columnVirtualizer}
          table={props.table}
          virtualPaddingLeft={virtualPaddingLeft()}
          virtualPaddingRight={virtualPaddingRight()}
        />
        <TableBody
          columnVirtualizer={columnVirtualizer}
          rowVirtualizer={rowVirtualizer}
          rows={rows}
          table={props.table}
          virtualPaddingLeft={virtualPaddingLeft()}
          virtualPaddingRight={virtualPaddingRight()}
        />
      </table>
    </div>
  )
}

function TableHead(props: {
  columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
  table: SolidTable<typeof _features, Person>
  virtualPaddingLeft: number | undefined
  virtualPaddingRight: number | undefined
}) {
  return (
    <thead
      style={{
        display: 'grid',
        position: 'sticky',
        top: '0px',
        'z-index': 1,
      }}
    >
      <For each={props.table.getHeaderGroups()}>
        {(headerGroup) => (
          <TableHeadRow
            columnVirtualizer={props.columnVirtualizer}
            headerGroup={headerGroup}
            virtualPaddingLeft={props.virtualPaddingLeft}
            virtualPaddingRight={props.virtualPaddingRight}
            table={props.table}
          />
        )}
      </For>
    </thead>
  )
}

function TableHeadRow(props: {
  columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
  headerGroup: HeaderGroup<typeof _features, Person>
  virtualPaddingLeft: number | undefined
  virtualPaddingRight: number | undefined
  table: SolidTable<typeof _features, Person>
}) {
  const virtualColumns = () => props.columnVirtualizer.getVirtualItems()
  return (
    <tr style={{ display: 'flex', width: '100%' }}>
      {props.virtualPaddingLeft ? (
        // fake empty column to the left for virtualization scroll padding
        <th
          style={{ display: 'flex', width: `${props.virtualPaddingLeft}px` }}
        />
      ) : null}
      <For each={virtualColumns()}>
        {(virtualColumn) => {
          const header = props.headerGroup.headers[virtualColumn.index]
          return <TableHeadCell header={header} table={props.table} />
        }}
      </For>
      {props.virtualPaddingRight ? (
        // fake empty column to the right for virtualization scroll padding
        <th
          style={{ display: 'flex', width: `${props.virtualPaddingRight}px` }}
        />
      ) : null}
    </tr>
  )
}

function TableHeadCell(props: {
  header: Header<typeof _features, Person, unknown>
  table: SolidTable<typeof _features, Person>
}) {
  return (
    <th
      style={{
        display: 'flex',
        width: `${props.header.getSize()}px`,
      }}
    >
      <div
        class={
          props.header.column.getCanSort() ? 'cursor-pointer select-none' : ''
        }
        onClick={props.header.column.getToggleSortingHandler()}
      >
        <FlexRender header={props.header} />
        {(
          {
            asc: ' 🔼',
            desc: ' 🔽',
          } as Record<string, string>
        )[props.header.column.getIsSorted() as string] ?? null}
      </div>
    </th>
  )
}

function TableBody(props: {
  columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
  rowVirtualizer: Virtualizer<HTMLDivElement, HTMLTableRowElement>
  rows: () => Array<Row<typeof _features, Person>>
  table: SolidTable<typeof _features, Person>
  virtualPaddingLeft: number | undefined
  virtualPaddingRight: number | undefined
}) {
  const virtualRows = () => props.rowVirtualizer.getVirtualItems()

  return (
    <tbody
      style={{
        display: 'grid',
        height: `${props.rowVirtualizer.getTotalSize()}px`, // tells scrollbar how big the table is
        position: 'relative', // needed for absolute positioning of rows
      }}
    >
      <For each={virtualRows()}>
        {(virtualRow) => {
          const row = props.rows()[virtualRow.index]
          return (
            <TableBodyRow
              columnVirtualizer={props.columnVirtualizer}
              row={row}
              rowVirtualizer={props.rowVirtualizer}
              virtualPaddingLeft={props.virtualPaddingLeft}
              virtualPaddingRight={props.virtualPaddingRight}
              virtualRow={virtualRow}
              table={props.table}
            />
          )
        }}
      </For>
    </tbody>
  )
}

function TableBodyRow(props: {
  columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
  row: Row<typeof _features, Person>
  rowVirtualizer: Virtualizer<HTMLDivElement, HTMLTableRowElement>
  virtualPaddingLeft: number | undefined
  virtualPaddingRight: number | undefined
  virtualRow: VirtualItem
  table: SolidTable<typeof _features, Person>
}) {
  const visibleCells = () => props.row.getVisibleCells()
  const virtualColumns = () => props.columnVirtualizer.getVirtualItems()
  return (
    <tr
      data-index={props.virtualRow.index} // needed for dynamic row height measurement
      ref={(node) => props.rowVirtualizer.measureElement(node)} // measure dynamic row height
      style={{
        display: 'flex',
        position: 'absolute',
        transform: `translateY(${props.virtualRow.start}px)`, // this should always be a `style` as it changes on scroll
        width: '100%',
      }}
    >
      {props.virtualPaddingLeft ? (
        // fake empty column to the left for virtualization scroll padding
        <td
          style={{ display: 'flex', width: `${props.virtualPaddingLeft}px` }}
        />
      ) : null}
      <For each={virtualColumns()}>
        {(vc) => {
          const cell = visibleCells()[vc.index]
          return <TableBodyCell cell={cell} table={props.table} />
        }}
      </For>
      {props.virtualPaddingRight ? (
        // fake empty column to the right for virtualization scroll padding
        <td
          style={{ display: 'flex', width: `${props.virtualPaddingRight}px` }}
        />
      ) : null}
    </tr>
  )
}

function TableBodyCell(props: {
  cell: Cell<typeof _features, Person, unknown>
  table: SolidTable<typeof _features, Person>
}) {
  return (
    <td
      style={{
        display: 'flex',
        width: `${props.cell.column.getSize()}px`,
      }}
    >
      <FlexRender cell={props.cell} />
    </td>
  )
}

export default App
import {
  FlexRender,
  columnSizingFeature,
  columnVisibilityFeature,
  createSortedRowModel,
  createTable,
  rowSortingFeature,
  sortFns,
  tableFeatures,
} from '@tanstack/solid-table'
import { createVirtualizer } from '@tanstack/solid-virtual'
import { For, createSignal } from 'solid-js'
import { makeColumns, makeData } from './makeData'
import type {
  Cell,
  Header,
  HeaderGroup,
  Row,
  SolidTable,
} from '@tanstack/solid-table'
import type { VirtualItem, Virtualizer } from '@tanstack/solid-virtual'
import type { Person } from './makeData'

const _features = tableFeatures({
  columnSizingFeature,
  columnVisibilityFeature,
  rowSortingFeature,
})

function App() {
  const columns = makeColumns(1_000)
  const [data, setData] = createSignal(makeData(1_000, columns))

  const refreshData = () => setData(makeData(1_000, columns))

  const table = createTable({
    _features,
    _rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
    columns,
    get data() {
      return data()
    },
    debugTable: true,
  })

  return (
    <div class="app">
      <div>({columns.length.toLocaleString()} columns)</div>
      <div>({data().length.toLocaleString()} rows)</div>
      <button onClick={refreshData}>Refresh Data</button>
      <TableContainer table={table} />
    </div>
  )
}

// Important: Keep both virtualizers and the scroll container ref in the same component.
// The ref must be undefined when createVirtualizer runs (before JSX return),
// so that onMount can set up scroll observers after the element is in the DOM.
function TableContainer(props: {
  table: SolidTable<typeof _features, Person>
}) {
  const visibleColumns = () => props.table.getVisibleLeafColumns()
  const rows = () => props.table.getRowModel().rows

  let tableContainerRef: HTMLDivElement | undefined

  // We are using a slightly different virtualization strategy for columns (compared to virtual rows)
  // in order to support dynamic row heights.
  const columnVirtualizer = createVirtualizer<
    HTMLDivElement,
    HTMLTableCellElement
  >({
    get count() {
      return visibleColumns().length
    },
    estimateSize: (index) => visibleColumns()[index].getSize(), // estimate width of each column for accurate scrollbar dragging
    getScrollElement: () => tableContainerRef ?? null,
    horizontal: true,
    overscan: 3, // how many columns to render on each side off screen (adjust this for performance)
  })

  // dynamic row height virtualization - alternatively you could use a simpler fixed row height strategy without `measureElement`
  const rowVirtualizer = createVirtualizer<HTMLDivElement, HTMLTableRowElement>(
    {
      get count() {
        return rows().length
      },
      estimateSize: () => 33, // estimate row height for accurate scrollbar dragging
      getScrollElement: () => tableContainerRef ?? null,
      // measure dynamic row height, except in firefox because it measures table border height incorrectly
      measureElement:
        typeof window !== 'undefined' &&
        navigator.userAgent.indexOf('Firefox') === -1
          ? (element) => element.getBoundingClientRect().height
          : undefined,
      overscan: 5,
    },
  )

  // Different virtualization strategy for columns - instead of absolute and translateY,
  // we add empty columns to the left and right
  const virtualPaddingLeft = () => {
    const vcs = columnVirtualizer.getVirtualItems()
    return vcs.length ? (vcs[0]?.start ?? 0) : undefined
  }

  const virtualPaddingRight = () => {
    const vcs = columnVirtualizer.getVirtualItems()
    if (!vcs.length) return undefined
    return columnVirtualizer.getTotalSize() - (vcs[vcs.length - 1]?.end ?? 0)
  }

  return (
    <div
      class="container"
      ref={tableContainerRef}
      style={{
        overflow: 'auto',
        position: 'relative',
        height: '800px',
      }}
    >
      {/* Even though we're still using semantic table tags, we must use CSS grid and flexbox for dynamic row heights */}
      <table style={{ display: 'grid' }}>
        <TableHead
          columnVirtualizer={columnVirtualizer}
          table={props.table}
          virtualPaddingLeft={virtualPaddingLeft()}
          virtualPaddingRight={virtualPaddingRight()}
        />
        <TableBody
          columnVirtualizer={columnVirtualizer}
          rowVirtualizer={rowVirtualizer}
          rows={rows}
          table={props.table}
          virtualPaddingLeft={virtualPaddingLeft()}
          virtualPaddingRight={virtualPaddingRight()}
        />
      </table>
    </div>
  )
}

function TableHead(props: {
  columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
  table: SolidTable<typeof _features, Person>
  virtualPaddingLeft: number | undefined
  virtualPaddingRight: number | undefined
}) {
  return (
    <thead
      style={{
        display: 'grid',
        position: 'sticky',
        top: '0px',
        'z-index': 1,
      }}
    >
      <For each={props.table.getHeaderGroups()}>
        {(headerGroup) => (
          <TableHeadRow
            columnVirtualizer={props.columnVirtualizer}
            headerGroup={headerGroup}
            virtualPaddingLeft={props.virtualPaddingLeft}
            virtualPaddingRight={props.virtualPaddingRight}
            table={props.table}
          />
        )}
      </For>
    </thead>
  )
}

function TableHeadRow(props: {
  columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
  headerGroup: HeaderGroup<typeof _features, Person>
  virtualPaddingLeft: number | undefined
  virtualPaddingRight: number | undefined
  table: SolidTable<typeof _features, Person>
}) {
  const virtualColumns = () => props.columnVirtualizer.getVirtualItems()
  return (
    <tr style={{ display: 'flex', width: '100%' }}>
      {props.virtualPaddingLeft ? (
        // fake empty column to the left for virtualization scroll padding
        <th
          style={{ display: 'flex', width: `${props.virtualPaddingLeft}px` }}
        />
      ) : null}
      <For each={virtualColumns()}>
        {(virtualColumn) => {
          const header = props.headerGroup.headers[virtualColumn.index]
          return <TableHeadCell header={header} table={props.table} />
        }}
      </For>
      {props.virtualPaddingRight ? (
        // fake empty column to the right for virtualization scroll padding
        <th
          style={{ display: 'flex', width: `${props.virtualPaddingRight}px` }}
        />
      ) : null}
    </tr>
  )
}

function TableHeadCell(props: {
  header: Header<typeof _features, Person, unknown>
  table: SolidTable<typeof _features, Person>
}) {
  return (
    <th
      style={{
        display: 'flex',
        width: `${props.header.getSize()}px`,
      }}
    >
      <div
        class={
          props.header.column.getCanSort() ? 'cursor-pointer select-none' : ''
        }
        onClick={props.header.column.getToggleSortingHandler()}
      >
        <FlexRender header={props.header} />
        {(
          {
            asc: ' 🔼',
            desc: ' 🔽',
          } as Record<string, string>
        )[props.header.column.getIsSorted() as string] ?? null}
      </div>
    </th>
  )
}

function TableBody(props: {
  columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
  rowVirtualizer: Virtualizer<HTMLDivElement, HTMLTableRowElement>
  rows: () => Array<Row<typeof _features, Person>>
  table: SolidTable<typeof _features, Person>
  virtualPaddingLeft: number | undefined
  virtualPaddingRight: number | undefined
}) {
  const virtualRows = () => props.rowVirtualizer.getVirtualItems()

  return (
    <tbody
      style={{
        display: 'grid',
        height: `${props.rowVirtualizer.getTotalSize()}px`, // tells scrollbar how big the table is
        position: 'relative', // needed for absolute positioning of rows
      }}
    >
      <For each={virtualRows()}>
        {(virtualRow) => {
          const row = props.rows()[virtualRow.index]
          return (
            <TableBodyRow
              columnVirtualizer={props.columnVirtualizer}
              row={row}
              rowVirtualizer={props.rowVirtualizer}
              virtualPaddingLeft={props.virtualPaddingLeft}
              virtualPaddingRight={props.virtualPaddingRight}
              virtualRow={virtualRow}
              table={props.table}
            />
          )
        }}
      </For>
    </tbody>
  )
}

function TableBodyRow(props: {
  columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
  row: Row<typeof _features, Person>
  rowVirtualizer: Virtualizer<HTMLDivElement, HTMLTableRowElement>
  virtualPaddingLeft: number | undefined
  virtualPaddingRight: number | undefined
  virtualRow: VirtualItem
  table: SolidTable<typeof _features, Person>
}) {
  const visibleCells = () => props.row.getVisibleCells()
  const virtualColumns = () => props.columnVirtualizer.getVirtualItems()
  return (
    <tr
      data-index={props.virtualRow.index} // needed for dynamic row height measurement
      ref={(node) => props.rowVirtualizer.measureElement(node)} // measure dynamic row height
      style={{
        display: 'flex',
        position: 'absolute',
        transform: `translateY(${props.virtualRow.start}px)`, // this should always be a `style` as it changes on scroll
        width: '100%',
      }}
    >
      {props.virtualPaddingLeft ? (
        // fake empty column to the left for virtualization scroll padding
        <td
          style={{ display: 'flex', width: `${props.virtualPaddingLeft}px` }}
        />
      ) : null}
      <For each={virtualColumns()}>
        {(vc) => {
          const cell = visibleCells()[vc.index]
          return <TableBodyCell cell={cell} table={props.table} />
        }}
      </For>
      {props.virtualPaddingRight ? (
        // fake empty column to the right for virtualization scroll padding
        <td
          style={{ display: 'flex', width: `${props.virtualPaddingRight}px` }}
        />
      ) : null}
    </tr>
  )
}

function TableBodyCell(props: {
  cell: Cell<typeof _features, Person, unknown>
  table: SolidTable<typeof _features, Person>
}) {
  return (
    <td
      style={{
        display: 'flex',
        width: `${props.cell.column.getSize()}px`,
      }}
    >
      <FlexRender cell={props.cell} />
    </td>
  )
}

export default App