Skip to content

Scroll Containers

The default table renders its own scrolling element. Three props change that — pick one when the table shares a viewport with surrounding page chrome, or when virtualization needs to be driven by a different scrolling element.

import * as React from 'react'

import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { localModel } from '@virtuoso.dev/data-table'

const rows = Array.from({ length: 80 }, (_, index) => ({
  id: `SKU-${String(index + 1).padStart(3, '0')}`,
  name: `Product ${index + 1}`,
  category: ['Office', 'Peripherals', 'Audio'][index % 3]!,
}))

const model = localModel({ data: rows })

export default function App() {
  const [scrollParent, setScrollParent] = React.useState<HTMLDivElement | null>(null)

  return (
    <div ref={setScrollParent} className="h-[360px] overflow-auto rounded-xl border">
      <div className="sticky top-0 z-10 border-b bg-background px-3 py-2 text-sm font-medium">Inventory workspace</div>
      <DataTable computeRowKey={({ data }) => data.id} customScrollParent={scrollParent} increaseViewportBy={360} model={model}>
        <DataTableColumn field="name">
          <DataTableColumnHeader>Product</DataTableColumnHeader>
          <DataTableCell className="font-medium">{({ cellValue }) => String(cellValue)}</DataTableCell>
        </DataTableColumn>
        <DataTableColumn field="category">
          <DataTableColumnHeader>Category</DataTableColumnHeader>
          <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
        </DataTableColumn>
      </DataTable>
    </div>
  )
}

useWindowScroll makes the document viewport drive the table’s scroll state:

<DataTable model={model} useWindowScroll>
  {/* columns */}
</DataTable>

The table no longer renders an internal scroller; its height comes from rendered content and the surrounding page layout. Sticky headers now interact with document-level sticky offsets, so test them against your actual header/nav stack.

customScrollParent attaches the table to an existing scrolling element:

const [scrollParent, setScrollParent] = React.useState<HTMLDivElement | null>(null)

return (
  <div ref={setScrollParent} className="h-[480px] overflow-auto">
    <Toolbar />
    <DataTable customScrollParent={scrollParent} model={model}>
      {/* columns */}
    </DataTable>
  </div>
)

ScrollElement replaces the default scroller component while keeping the rest of the table contained:

const ScrollElement = React.forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement>>(function ScrollElement(props, ref) {
  return <div ref={ref} className="custom-scrollbar overflow-auto" {...props} />
})

<DataTable ScrollElement={ScrollElement} model={model} />

Pick one scroll mode per table. For customScrollParent, make sure that element has a real height and is the element that actually scrolls.