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 className="rounded-xl border-2 border-border/70 shadow-sm" 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
  • 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.

@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-reorder-grip, data-table-reorder-drop-zone, data-table-draggable-group-header — reorder UI

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 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.