Skip to content

Embed a Table in a Shared Scroll Area

The table renders inside a parent with its own overflow: auto, so notes, filters, and the table body all scroll together. customScrollParent tells the table to drive virtualization from that outer element instead of its own scroller.

  • customScrollParent — attaches the table to an external scrolling element
  • An outer container with overflow: auto and a real height
import { useState } from 'react'

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

import { products } from './data'
import { localModel } from '@virtuoso.dev/data-table'

const model = localModel({ data: products })

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

  return (
    <div className="rounded-xl">
      <div className="border-b px-4 py-3">
        <p className="font-semibold">Shared scroll surface</p>
        <p className="text-sm text-muted-foreground">The note above the table scrolls together with the rows.</p>
      </div>

      <div className="h-[420px] overflow-auto" ref={setScrollParent}>
        <div className="border-b px-4 py-3 text-sm text-muted-foreground">
          Filters, charts, or help text can live in the same scrolling context as the table body.
        </div>

        <DataTable className="min-w-[720px]" customScrollParent={scrollParent} model={model}>
          <DataTableColumn field="name">
            <DataTableColumnHeader>Product</DataTableColumnHeader>
            <DataTableCell className="font-medium">{({ row }) => row.data.name}</DataTableCell>
          </DataTableColumn>
          <DataTableColumn field="category">
            <DataTableColumnHeader>Category</DataTableColumnHeader>
            <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
          </DataTableColumn>
          <DataTableColumn field="region">
            <DataTableColumnHeader>Region</DataTableColumnHeader>
            <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
          </DataTableColumn>
          <DataTableColumn field="status">
            <DataTableColumnHeader>Status</DataTableColumnHeader>
            <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
          </DataTableColumn>
        </DataTable>
      </div>
    </div>
  )
}

The outer container’s height bounds the entire layout, including the table. Sticky table headers now interact with sibling sticky elements inside the same scroll context — test both together.

Do not set style={{ height }} on the table when customScrollParent is in use. The table grows to its virtualized content height; the outer container’s scroll height drives virtualization. A fixed table height clips the rows before the scroll parent can reach them, and the bottom rows appear to never load.