petyosi/react-virtuoso/data-table

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Build virtualized data tables with @virtuoso.dev/data-table. Use this skill when (1) building a data grid with sorting, filtering, or grouped rows, (2) installing the shadcn-styled or headless table, (3) connecting remote/paginated data sources, (4) adding sticky, resizable, reorderable, or hideable columns, (5) persisting table state, (6) controlling a table from outside (scrolling, actions), (7) migrating from TableVirtuoso, or any task involving VirtuosoDataTable, DataTable, DataTableColumn, localModel, remoteModel, or engine refs like scrollToRow$ and dispatchModelAction$.

Install

npx vskill@latest install petyosi/react-virtuoso/data-table

SKILL.md

@virtuoso.dev/data-table

A virtualized React data table (rows and columns) with grouped rows, sticky columns, column resizing/reordering/visibility, state persistence, and remote data support. It is the successor to TableVirtuoso for table-shaped problems: instead of row renderers, you pass a data source (model) and declare columns as JSX.

Installation: two paths

Shadcn (pre-styled wrapper) — for projects using shadcn/ui conventions:

bash
npx shadcn@latest add petyosi/react-virtuoso/data-table

This installs a styled wrapper at @/components/ui/data-table exporting DataTable, DataTableColumn, DataTableColumnHeader, DataTableCell. Optional feature UI parts are separate registry items:

bash
npx shadcn@latest add petyosi/react-virtuoso/data-table-resize-handle
npx shadcn@latest add petyosi/react-virtuoso/data-table-sort-header-button

The registry item names are hyphenated. The installed imports remain nested (@/components/ui/data-table/column-resize, @/components/ui/data-table/column-sort). Do not run path-shaped shadcn commands such as data-table/column-resize or data-table/column-sort; shadcn resolves registry item names, not import paths.

Headless — for custom design systems:

bash
npm install @virtuoso.dev/data-table

Import the structural styles (@import '@virtuoso.dev/data-table/styles.css') and use the unstyled VirtuosoDataTable, Column, ColumnHeader, Cell. Ask which path fits the project before installing; the shadcn wrapper is the faster start in Tailwind/shadcn codebases.

Minimal example (shadcn wrapper)

tsx
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { localModel } from '@virtuoso.dev/data-table'
const model = localModel({ data: products })
export default function App() {
return (
<DataTable model={model} style={{ height: 360 }}>
<DataTableColumn field="name">
<DataTableColumnHeader>Product</DataTableColumnHeader>
<DataTableCell className="font-medium">{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
</DataTable>
)
}

Columns are JSX, not config objects. field is both the row-data lookup key and the column's public identifier (used by visibility, reordering, persistence). The table needs a real height, like every Virtuoso component.

Treat field and id as stable column identities, not as user-facing labels. Add an explicit DataTableColumnHeader for every visible column. This is especially important for display-only columns such as actions: use a visible label like Actions unless the product intentionally wants an icon-only/headerless column, in which case use sr-only text for accessibility.

When a table should fill the remaining height in a page, panel, or card, use a measured flex column instead of a fixed pixel height. Every flex ancestor between the measured container and the table needs min-h-0; non-table chrome should be shrink-0; the table should be the growing child with style={{ height: '100%' }}:

tsx
<section className="flex h-full min-h-0 flex-col">
<PageHeader className="shrink-0" />
<div className="flex min-h-0 flex-1 flex-col gap-3">
<Toolbar className="shrink-0" />
<DataTable className="min-h-0 flex-1" model={model} style={{ height: '100%' }}>
{/* columns */}
</DataTable>
</div>
</section>

This is optional. If the parent does not have a definite height, keep using a fixed height (style={{ height: 360 }}) or choose useWindowScroll / customScrollParent deliberately.

Column widths are owned by the table through header measurements. Put base width classes such as w-*, min-w-*, and basis-* on DataTableColumnHeader, not on DataTableCell. Cells render inside tracks sized from header measurements; cell width utilities can force body content outside those tracks and make columns overlap at narrow widths. Use cell className for typography, alignment, padding, truncation, and color. For complex cell content, put min-w-0 on an inner wrapper instead of widening the cell.

A growing header has separate measurement and presentation surfaces. The table measures the intrinsic header first, then adds spare width according to grow. If that same measured element stretches across the rendered column, ResizeObserver reports the grown width as the next base width and the column cannot shrink when its viewport narrows.

  • Use DataTableColumnHeader.className for the intrinsic surface: fixed or minimum base width, padding, typography, and alignment within that base width.
  • Use DataTableColumnHeader.containerProps for paint and interaction across the rendered column: background, hover/focus state, outline, box shadow, cursor, ARIA attributes, and event handlers.
  • Do not use width, flex, margin, padding, border, display, or justify-* styles in containerProps. The table owns the outer geometry; layout styles there can clip content and move HeaderEnd or HeaderEdge slots. Use box-shadow or outline for full-width separators.
  • Events on containerProps receive bubbled events from sort buttons, resize handles, reorder controls, and other nested UI. Stop propagation in the nested control or guard the container handler before running a whole-header action.
tsx
<DataTableColumn field="name" grow={1}>
<DataTableColumnHeader className="min-w-72" containerProps={{ className: 'cursor-pointer hover:bg-muted/50 focus-within:outline' }}>
Name
</DataTableColumnHeader>
<DataTableCell>{NameCell}</DataTableCell>
</DataTableColumn>

Do not use w-full or flex growth on className to paint the full rendered track. Those styles resolve inside the intrinsic measurement boundary by design; put the full-width visual on containerProps. Use HeaderEnd for a control that must align to the rendered column edge.

When building a table, choose fixed vs growing columns from the data being displayed:

  • Keep compact columns fixed by omitting grow: ids, slugs, checkboxes, action menus, icon buttons, status labels, badges, versions, counts, dates, timestamps, and short enum or metadata columns.
  • Mark text-heavy columns as growing with DataTableColumn grow={number}: names, titles, descriptions, summaries, messages, notes, paths, and other content where extra horizontal space improves scanning or reduces truncation.
  • Use positive finite grow values. grow={0} is equivalent to omitting grow; prefer omission for fixed columns unless a generated config shape needs an explicit value.
  • Ground the choice in local understanding of the table. A name column often grows, but a short code-like name may be fixed; a label column is often fixed, but user-authored labels may grow.
  • Do not make every column grow. grow protects compact columns from absorbing leftover space while useful text columns take the room.

Example:

tsx
<DataTableColumn field="name" grow={1}>
<DataTableColumnHeader className="min-w-72">Name</DataTableColumnHeader>
<DataTableCell>{NameCell}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="description" grow={3}>
<DataTableColumnHeader className="min-w-80">Description</DataTableColumnHeader>
<DataTableCell>{DescriptionCell}</DataTableCell>
</DataTableColumn>
<DataTableColumn id="actions">
<DataTableColumnHeader className="w-16 justify-center">Actions</DataTableColumnHeader>
<DataTableCell>{ActionsCell}</DataTableCell>
</DataTableColumn>

Choosing a data model

SituationModel
Rows in memory; filter/sort/group client-sidelocalModel({ data, pipeline?, actions?, groups? })
API-backed, known total count (placeholder rows while fetching)remoteModel with offset mode (defaultOffsetViewportHandler); fetch returns { rows, totalCount }
API-backed, cursor pagination / infinite appendremoteModel with append mode (defaultAppendViewportHandler); fetch returns { rows }

Hold the model in useState with lazy init — const [model] = useState(() => localModel({ data })). Do not use useMemo; React may discard memoized values, and the model instance must be retained. Module scope is fine for a static singleton table.

Local filtering/sorting/grouping runs through a named-stage pipeline: declare pipeline: ['filter', 'sort'] plus actions, then dispatch with model.send({ action: 'filter', payload }). See local-data-model and the local-filter-sort-group example.

For remote sorting/filtering/search controls, keep the action payload in the model rather than mirroring it in React state. Seed defaults with initialActions, dispatch changes with model.send() or dispatchModelAction$, and read modelActionState$ to paint active controls. This is especially important when modelStatePersistenceAdapter() restores saved action state.

Provide computeRowKey={({ data }) => data.id} whenever rows can reorder (sort, filter, remote updates) — without it rows remount and lose local state.

Column features

  • Sticky columns: <DataTableColumn field="name" sticky="left" /> (or "right"); multiple sticky columns stack.
  • Visibility: declaratively via visible={false}, or at runtime through setColumnVisibility$ / columnVisibilityState$ from @virtuoso.dev/data-table/column-visibility.
  • Resizing: mount ResizeHandle in the HeaderEdge slot; programmatic via resizeColumn$ from @virtuoso.dev/data-table/column-resize.
  • Reordering: ReorderGrip in HeaderStart + ReorderDropZone in HeaderOverlay; programmatic via reorderColumns$ from @virtuoso.dev/data-table/column-reorder.
  • Header slots: HeaderStart (before label), HeaderEnd (after label), HeaderEdge (pinned to the column boundary), HeaderOverlay (covers the header) — see header-slots.
  • Grouped rows: pass groups: [{ index, level }] alongside data and render headers with GroupHeaderCell — see grouped-rows.

State persistence: mount <DataTableStatePersistence adapters={[...]} storageKey="my-table" /> with adapters from the feature subpaths (columnVisibilityPersistenceAdapter(), columnOrderPersistenceAdapter(), columnWidthPersistenceAdapter(), modelStatePersistenceAdapter()). See state-persistence.

Controlling the table from outside

The table's state lives in an internal reactive engine, and the package intentionally exports cells (readable state, $-suffixed) and streams (actions) for remote control — state is not lifted into props:

tsx
import { scrollLocation$, scrollToRow$, useEngineRef, useRemoteCellValue, useRemotePublisher } from '@virtuoso.dev/data-table'
const engineRef = useEngineRef()
const scrollToRow = useRemotePublisher(scrollToRow$, engineRef)
const location = useRemoteCellValue(scrollLocation$, engineRef)
<DataTable engineRef={engineRef} model={model}>...</DataTable>
<button onClick={() => scrollToRow({ index: 100, align: 'start' })}>Go to row 100</button>

For UI far from the table, pass engineId="orders-table" and use the same hooks with the string id. Useful nodes: scrollToRow$, scrollIntoView$, setColumnVisibility$, dispatchModelAction$ (actions); scrollLocation$, columns$, columnVisibilityState$, modelActionState$, loadingState$ (state). See controlling-the-table.

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
6,330
Repository forks
348
7-day trend score
44

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/petyosi/react-virtuoso/data-table/badge)](https://verified-skill.com/skills/petyosi/react-virtuoso/data-table)

Something wrong with this skill or its page? Tell us.