Skip to content

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>
  )
}

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.

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.

VariableDefaultUsed for
--data-table-bghsl(var(--background))Table body, sticky header, sticky column wrappers
--data-table-fghsl(var(--foreground))Default table/header/group-header text
--data-table-borderhsl(var(--border))Row separators, header borders, loading containers
--data-table-mutedhsl(var(--muted))Group rows, skeleton rows, sticky hover blends
--data-table-muted-fghsl(var(--muted-foreground))Loading and secondary wrapper text
--data-table-row-hoverhsl(var(--muted) / 0.5)Non-sticky row hover background
--data-table-sticky-hovera mix of muted and table bgSticky 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}
/>

@virtuoso.dev/data-table covers behavior:

  • virtualization, measurement, scrolling
  • localModel() and remoteModel()
  • 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.

Some opt-in features ship their own wrapper components, following the same presentation/behavior split:

  • data-table-resize-handle — resize handle UI
  • data-table-sort-header-button — model-backed sort button UI
  • data-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.

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, LoadingPlaceholder your 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.