CtrlK
BlogDocsLog inGet started
Tessl Logo

with-tanstack-virtual

Virtualize Lit Table final row or column models with @tanstack/lit-virtual VirtualizerController, host lifecycle-aware counts, scroll refs, stable keys, dynamic measurement, sticky CSS, grid/flex sizing, and infinite data. Load for large rendered tables; Virtual is renderer composition, not tableFeatures.

64

Quality

77%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Fix and improve this skill with Tessl

tessl review fix ./packages/lit-table/skills/with-tanstack-virtual/SKILL.md
SKILL.md
Quality
Evals
Security

This skill builds on @tanstack/table-core#core plus this package's getting-started and table-state skills.

Setup

import { LitElement, html } from 'lit'
import { customElement } from 'lit/decorators.js'
import { createRef, ref } from 'lit/directives/ref.js'
import { VirtualizerController } from '@tanstack/lit-virtual'
import {
  TableController,
  tableFeatures,
  type ColumnDef,
} from '@tanstack/lit-table'

type Item = { id: string; name: string }
const features = tableFeatures({})
const columns: Array<ColumnDef<typeof features, Item>> = [
  { accessorKey: 'name', header: 'Name' },
]
const data: Array<Item> = Array.from({ length: 10_000 }, (_, id) => ({
  id: String(id),
  name: `Item ${id}`,
}))

@customElement('virtual-items')
export class VirtualItems extends LitElement {
  private controller = new TableController<typeof features, Item>(this)
  private scroller = createRef<HTMLDivElement>()
  private virtualizer = new VirtualizerController(this, {
    count: data.length,
    getScrollElement: () => this.scroller.value,
    estimateSize: () => 32,
    overscan: 5,
  })

  protected render() {
    const table = this.controller.table({
      features,
      columns,
      data,
      getRowId: (row) => row.id,
    })
    const rows = table.getRowModel().rows
    const v = this.virtualizer.getVirtualizer()
    v.setOptions({ ...v.options, count: rows.length })

    return html`<div
      ${ref(this.scroller)}
      style="height:400px;overflow:auto;position:relative"
    >
      <div style="height:${v.getTotalSize()}px;position:relative">
        ${v
          .getVirtualItems()
          .map(
            (item) =>
              html`<div
                style="position:absolute;transform:translateY(${item.start}px)"
              >
                ${rows[item.index].getValue('name')}
              </div>`,
          )}
      </div>
    </div>`
  }
}

Core Patterns

Virtualize the final row model

Set the virtualizer count from table.getRowModel().rows.length; index virtual items into that same array so filtering, sorting, expansion, and pagination are respected.

Keep geometry sources consistent

For column virtualization, estimate from column.getSize() and remeasure when columnSizing changes. Render widths from the same sizing state.

Treat unsupported combinations as user composition

Drag-and-drop plus virtualization is not a maintained Table recipe. Start from the maintained Virtual example and reconcile both libraries' transforms, measurement, and auto-scroll contracts explicitly.

Common Mistakes

HIGH Virtualizing raw data

Wrong: index virtual items into the original data array.

Correct: index into table.getRowModel().rows.

Raw data ignores active Table processing and produces mismatched rows after sorting or filtering.

Source: TanStack/table:examples/lit/virtualized-rows

HIGH Count and rows come from different models

Wrong: use data.length after filters change but render filtered rows.

Correct: update the virtualizer count from the same current rows array being rendered.

Mismatched geometry can index undefined rows or leave blank scroll space.

Source: TanStack/table:examples/lit/virtualized-rows

HIGH Recreating columns during host renders

Wrong: pass columns: [...columns] from render() to work around a readonly tuple.

Correct: declare a stable mutable Array<ColumnDef<typeof features, Item>> once and pass columns directly.

Lit host updates can be driven by scrolling and measurement. A fresh column reference rebuilds Table's column pipeline on every such render.

Source: TanStack/table:examples/lit/virtualized-rows

HIGH Sticky and sizing CSS assumed automatic

Wrong: add a VirtualizerController and expect sticky headers, widths, and transforms.

Correct: implement the scroll container, total-size spacer, absolute row transforms, sticky regions, and widths in Lit CSS/templates.

Both libraries are headless over rendering geometry.

Source: TanStack/table:docs/framework/lit/guide/virtualization.md

API Discovery

Inspect node_modules/@tanstack/lit-table/dist/index.d.ts and node_modules/@tanstack/lit-virtual/dist/. Use maintained examples for geometry; do not register Virtual in tableFeatures.

Repository
TanStack/table
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.