Inside the shadcn Wrapper
This page applies to the shadcn install path. If you installed @virtuoso.dev/data-table directly without the registry, you own the equivalent components in your own files — the wrapper-vs-headless split still applies, but the file locations are yours to decide. See Headless Install for that route.
Running npx shadcn add copied a folder of components into @/components/ui/data-table. That folder is your code — it ships in your repo, it’s checked into your git history, and editing it doesn’t fork the table’s behavior. The behavior lives somewhere else.
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { localModel } from '@virtuoso.dev/data-table'
const rows = [
{ product: 'Standing Desk', owner: 'Avery', status: 'Launch ready' },
{ product: 'USB-C Dock', owner: 'Morgan', status: 'In review' },
{ product: 'Mechanical Keyboard', owner: 'Riley', status: 'Backorder' },
]
const model = localModel({ data: rows })
export default function App() {
return (
<DataTable model={model} style={{ height: 280 }}>
<DataTableColumn field="product">
<DataTableColumnHeader>Product</DataTableColumnHeader>
<DataTableCell className="font-medium">{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="owner">
<DataTableColumnHeader>Owner</DataTableColumnHeader>
<DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="status">
<DataTableColumnHeader>Status</DataTableColumnHeader>
<DataTableCell>
{({ cellValue }) => <span className="rounded-full bg-muted px-2 py-1 text-xs font-medium">{String(cellValue)}</span>}
</DataTableCell>
</DataTableColumn>
</DataTable>
)
}What the wrapper file owns
Section titled “What the wrapper file owns”The wrapper covers presentation:
- default row, header, sticky-column, empty, and loading components
- Tailwind classes for cells, headers, and group headers
- the default table frame (
rounded-md border) and table surface variables - ergonomic names (
DataTable,DataTableColumn,DataTableColumnHeader,DataTableCell) - re-exports of the headless hooks and helpers you’ll use day-to-day
Edit it like any other component in your app. Tailwind classes, default props, alternate variants, additional re-exports — all fair game. The wrapper file imports behavior from @virtuoso.dev/data-table, so your styling edits never fork table mechanics.
Table surface variables
Section titled “Table surface variables”The default shadcn wrapper gives DataTable an app-level frame (rounded-md border) and defines a small set of CSS variables on the same root. The frame border uses --data-table-border; callers do not need to add rounded-md border at each table instance. Use className only for intentional overrides such as rounded-xl, border-0, border-2, shadow-sm, or table variable overrides.
The variables matter most for sticky headers and sticky columns: those pieces are separate overlay containers, so they need an opaque background, but that background should match the table body instead of being hardcoded independently.
| Variable | Default | Used for |
|---|---|---|
--data-table-bg | hsl(var(--background)) | Table body, sticky header, sticky column wrappers |
--data-table-fg | hsl(var(--foreground)) | Default table/header/group-header text |
--data-table-border | hsl(var(--border)) | Row separators, header borders, loading containers |
--data-table-muted | hsl(var(--muted)) | Group rows, skeleton rows, sticky hover blends |
--data-table-muted-fg | hsl(var(--muted-foreground)) | Loading and secondary wrapper text |
--data-table-row-hover | hsl(var(--muted) / 0.5) | Non-sticky row hover background |
--data-table-sticky-hover | a mix of muted and table bg | Sticky column hover background |
If your app maps shadcn tokens to a different design system, override these variables once on the DataTable root or in the copied wrapper defaults. Do not style sticky cells individually just to fix a surface mismatch.
<DataTable
className="
[--data-table-bg:hsl(var(--surface-container-lowest))]
[--data-table-fg:hsl(var(--on-surface))]
[--data-table-border:hsl(var(--outline))]
[--data-table-muted:hsl(var(--surface-container-low))]
[--data-table-muted-fg:hsl(var(--on-surface-variant))]
"
model={model}
/>What the headless package owns
Section titled “What the headless package owns”@virtuoso.dev/data-table covers behavior:
- virtualization, measurement, scrolling
localModel()andremoteModel()- the reactive engine (
engineRef,useRemoteCellValue,useRemotePublisher) - opt-in feature modules (resize, reorder, visibility, persistence)
Behavior imports always come from @virtuoso.dev/data-table. If you find yourself reaching into the headless package to change behavior, that’s a signal you want an issue or a PR rather than a fork.
Feature-specific wrappers
Section titled “Feature-specific wrappers”Some opt-in features ship their own wrapper components, following the same presentation/behavior split:
data-table-resize-handle— resize handle UIdata-table-sort-header-button— model-backed sort button UIdata-table-reorder-grip,data-table-reorder-drop-zone,data-table-draggable-group-header— reorder UI
These names are registry item names, not import paths; shadcn installs them under nested paths such as @/components/ui/data-table/column-resize and @/components/ui/data-table/column-sort. Each one is header-slot UI you can restyle. The streams they publish to live in the corresponding @virtuoso.dev/data-table/... subpaths — see Header Slots for how those slots compose, and the column feature pages for what each registry component does.
When you’ll come back here
Section titled “When you’ll come back here”The wrapper is the right place to change:
- default Tailwind classes on
DataTable,DataTableCell,DataTableColumnHeader - the default table frame, radius, border, and surface variables
- the default
Row,EmptyPlaceholder,LoadingPlaceholderyour team should see - ergonomic re-exports you add for your codebase
For replacing the table’s structural components (Row, GroupHeader, sticky wrappers) on a per-instance basis without touching the wrapper file, see Replacing Internals.