data-table
Skill1 starsChanged 44 days agoMITView on GitHub
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$.
- Installs packages
3 other projects use a version of this.
Adopted by
Most recently taken up by
- petyosi/react-virtuosoword for word, first seen Oct 8, 2026
- metadist/synaplan-desktoptheir own version, first seen Sep 26, 2026
- vobase/vobasetheir own version, first seen Apr 25, 2026
Other versions3 projects use a version of this.
Word for word · 1
- petyosi/react-virtuoso★ 6.5k100% same
Edited · 0
None found.
Wrote their own version · 2
- metadist/synaplan-desktop★ 257same name
- vobase/vobase★ 60same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository3 other files it gives its agents.
- message-listSkill
- reactive-engineSkill
- react-virtuosoSkill
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 2 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 39
Import the structural styles (@import '@virtuoso.dev/data-table/styles.css') and use the unstyled VirtuosoDataTable, Column, ColumnHeader, …
- Very specific: line 85
Use DataTableColumnHeader.className for the intrinsic surface: fixed or minimum base width, padding, typography, and alignment within that …
- Very specific: line 86
Use DataTableColumnHeader.containerProps for paint and interaction across the rendered column: background, hover/focus state, outline, box …
- Very specific: line 87
Do not use width, flex, margin, padding, border, display, or justify-* styles in containerProps.
- Very specific: line 99
Do not use w-full or flex growth on className to paint the full rendered track.
- Distinctive: line 101
When building a table, choose fixed vs growing columns from the data being displayed:
- Very specific: line 103
Keep compact columns fixed by omitting grow: ids, slugs, checkboxes, action menus, icon buttons, status labels, badges, versions, counts, …
- Very specific: line 105
Use positive finite grow values.
- Very specific: line 107
Do not make every column grow.
- Distinctive: line 215
Before shipping a shadcn table, grep for width utilities on cells and move them to headers:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
@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:
npx shadcn@latest add petyosi/react-virtuoso/data-tableThis installs a styled wrapper at @/components/ui/data-table exporting DataTable, DataTableColumn, DataTableColumnHeader, DataTableCell.
Optional feature UI parts are separate registry items:
npx shadcn@latest add petyosi/react-virtuoso/data-table-resize-handle
npx shadcn@latest add petyosi/react-virtuoso/data-table-sort-header-buttonThe 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:
npm install @virtuoso.dev/data-tableImport 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)
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%' }}:
<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.classNamefor the intrinsic surface: fixed or minimum base width, padding, typography, and alignment within that base width. - Use
DataTableColumnHeader.containerPropsfor 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 incontainerProps. The table owns the outer geometry; layout styles there can clip content and moveHeaderEndorHeaderEdgeslots. Usebox-shadoworoutlinefor full-width separators. - Events on
containerPropsreceive 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.
<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 omittinggrow; prefer omission for fixed columns unless a generated config shape needs an explicit value. - Ground the choice in local understanding of the table. A
namecolumn often grows, but a short code-like name may be fixed; alabelcolumn is often fixed, but user-authored labels may grow. - Do not make every column grow.
growprotects compact columns from absorbing leftover space while useful text columns take the room.
Example:
<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
| Situation | Model |
| Rows in memory; filter/sort/group client-side | localModel({ 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 append | remoteModel 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 throughsetColumnVisibility$/columnVisibilityState$from@virtuoso.dev/data-table/column-visibility. - Resizing: mount
ResizeHandlein theHeaderEdgeslot; programmatic viaresizeColumn$from@virtuoso.dev/data-table/column-resize. - Reordering:
ReorderGripinHeaderStart+ReorderDropZoneinHeaderOverlay; programmatic viareorderColumns$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 }]alongsidedataand render headers withGroupHeaderCell— 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:
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.
Customization
- Styling goes through
classNameon the wrapper components and semantic data attributes — never usedata-testidas a styling hook. - For column headers, use
classNamefor intrinsic sizing and content layout, andcontainerPropsfor paint and interaction across the rendered column. Keep layout-changing styles offcontainerProps. - The shadcn wrapper already renders the app-level table frame (
rounded-md border) onDataTable. Do not addrounded-md borderat each table instance; useclassNameonly for intentional frame overrides such asrounded-xl,border-0,border-2, shadows, or table variable overrides. - The shadcn wrapper exposes table-level CSS variables (
--data-table-bg,--data-table-fg,--data-table-border,--data-table-muted,--data-table-muted-fg,--data-table-row-hover,--data-table-sticky-hover) and uses them for sticky headers, sticky columns, rows, and loading surfaces. When adapting to a host design system, override those variables once onDataTableor in the copied wrapper defaults instead of styling sticky cells individually. - Replace internals via the
componentsprop:Row,StickyColumnContainer,LoadingPlaceholder,LoadingOverlay,LoadingFooter(component overrides must forward refs). Top-level:EmptyPlaceholder,ScrollElement. context={{ ... }}flows tocomputeRowKey,EmptyPlaceholder, loading slots, and component overrides — but not to cell/header renderers (use React context there). See ambient-context.- Scroll modes: default internal scroller,
useWindowScroll, orcustomScrollParent— pick exactly one.
Migrating from TableVirtuoso
| TableVirtuoso | data-table |
data array | localModel({ data }) passed as model |
itemContent | DataTableColumn + DataTableCell |
fixedHeaderContent | DataTableColumnHeader |
| grouped rows | groups + GroupHeaderCell |
| fixed columns (CSS) | sticky="left" / sticky="right" |
ref + scrollToIndex | engineRef + scrollToRow$ |
rangeChanged | onRenderedDataChange, viewportRange$, scrollLocation$ |
Full guide: migrating-from-table-virtuoso.
Troubleshooting
| Symptom | Fix |
| Blank table or header only | Give the table a measurable height |
| Table does not fill its panel | Use the optional flex-height pattern: measured parent, min-h-0 ancestors, shrink-0 chrome, DataTable as flex-1 with height: '100%' |
| Shadcn component imports fail | Run the registry install, or import headless from @virtuoso.dev/data-table |
Shadcn creates a literal @/ directory | Make the alias resolvable from the root config shadcn reads; with solution-style tsconfigs, mirror @/* paths in root tsconfig.json |
| Page and table both scroll | Use only one scroll mode |
| Remote rows never appear | Return the right fetch shape ({ rows, totalCount } for offset mode) and pass the signal through |
| Rows remount / lose state after sorting | Add computeRowKey |
| Body cells overlap columns | Move width classes from DataTableCell to DataTableColumnHeader; use DataTableColumn grow={...} for extra width |
| Full-width header styles clip content | Keep layout on header className; use containerProps only for paint and interaction, without padding, borders, width, flex, or alignment |
| Action/display column has no header | Add an explicit visible DataTableColumnHeader label, e.g. Actions; field/id is an identity, not a UI label |
| Double outer border/frame | Remove call-site rounded-md border; the shadcn wrapper already owns the default table frame |
| Sticky/header colors don't match body | Override the shadcn wrapper's --data-table-* variables on DataTable or in the copied wrapper defaults |
| Sticky columns clipped | Check parent overflow and header min-widths |
| Empty cells flash on horizontal scroll | Raise columnOverscanCount |
Before shipping a shadcn table, grep for width utilities on cells and move them to headers:
rg 'DataTableCell.*className=.*(w-|min-w|max-w|basis-|grow|shrink|flex-(none|auto|initial|1|\[))'For tests, wrap in VirtuosoDataTableTestingContext.Provider value={{ itemHeight, viewportHeight }} (JSDOM has no layout) and assert behavior, not exact DOM row counts — overscan renders extra rows. Use real-browser tests for sticky columns, resizing, and drag interactions. See testing.
References
- references/README.md — overview
references/1.installation/— shadcn, headlessreferences/2.data-model/— local, remote, row-keysreferences/3.columns/— defining-columns, cell-and-header-renderers, column-groups, sticky-columns, visibility, resizing, reordering, runtime-columns, column-layout- Features: grouped-rows, state-persistence, controlling-the-table
references/7.customization/— styling, replacing-internals, empty-and-loading-states, scroll-containers, ambient-context, header-slots, shadcn-wrapperreferences/8.examples/— worked examples from basic table to remote pagination, dashboards, and persistencereferences/9.guides/— testing, performance, troubleshooting, migrating-from-table-virtuoso, debug-instrumentation
Full API reference: https://virtuoso.dev/data-table/
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

