Skip to content

Header Slots

A column header has four slot positions for additional UI. Resize handles, reorder grips, sort buttons, filter menus, and selection toggles all mount through one of them.

┌──────────────── HeaderOverlay ────────────────┐
│ [HeaderStart]  Header label  [HeaderEnd]  |Edge│
└───────────────────────────────────────────────┘
  • HeaderStart — before the label, in normal flex layout. Typical for buttons and menus that sit alongside the label text.
  • HeaderEnd — after the label, in normal flex layout. Same use cases as HeaderStart, opposite side.
  • HeaderEdge — pinned to the column boundary, outside the label’s flex flow. Resize handles use this; the position is independent of label length.
  • HeaderOverlay — covers the whole header. Drop indicators and custom overlays use this; the slot paints over both the label and any in-flow slots.
import * as React from 'react'

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

const rows = [
  { product: 'Standing Desk', category: 'Office', stock: 14 },
  { product: 'USB-C Dock', category: 'Peripherals', stock: 42 },
  { product: 'Mechanical Keyboard', category: 'Peripherals', stock: 28 },
]

const model = localModel({ data: rows })

export default function App() {
  const [sortField, setSortField] = React.useState<'product' | 'stock'>('product')
  const sortedRows = React.useMemo(() => {
    return [...rows].sort((a, b) => String(a[sortField]).localeCompare(String(b[sortField]), undefined, { numeric: true }))
  }, [sortField])

  React.useEffect(() => {
    model.setData(sortedRows)
  }, [sortedRows])

  return (
    <DataTable className="rounded-xl" model={model} style={{ height: 280 }}>
      <DataTableColumn field="product">
        <DataTableColumnHeader>
          <HeaderEnd>
            {({ column }) => (
              <button className="rounded border px-2 py-0.5 text-xs" onClick={() => setSortField(column.field as 'product')} type="button">
                {sortField === column.field ? 'Sorted' : 'Sort'}
              </button>
            )}
          </HeaderEnd>
          <HeaderEdge component={ResizeHandle} />
          {() => 'Product'}
        </DataTableColumnHeader>
        <DataTableCell className="font-medium">{({ cellValue }) => String(cellValue)}</DataTableCell>
      </DataTableColumn>
      <DataTableColumn field="stock">
        <DataTableColumnHeader className="justify-end">
          <HeaderEnd>
            {({ column }) => (
              <button className="rounded border px-2 py-0.5 text-xs" onClick={() => setSortField(column.field as 'stock')} type="button">
                {sortField === column.field ? 'Sorted' : 'Sort'}
              </button>
            )}
          </HeaderEnd>
          <HeaderEdge component={ResizeHandle} />
          {() => 'Stock'}
        </DataTableColumnHeader>
        <DataTableCell className="text-right tabular-nums">{({ cellValue }) => String(cellValue)}</DataTableCell>
      </DataTableColumn>
    </DataTable>
  )
}

Slots stack: a single header can host all four at once. Reorder grip on the left, sort button on the right, resize handle at the edge, drop indicator spanning the whole header for the reorder gesture.

import { HeaderEdge, HeaderEnd, HeaderOverlay, HeaderStart } from '@/components/ui/data-table'
import { ReorderDropZone, ReorderGrip } from '@/components/ui/data-table/column-reorder'
import { ResizeHandle } from '@/components/ui/data-table/column-resize'
;<DataTableColumn field="name">
  <DataTableColumnHeader>
    <HeaderStart component={ReorderGrip} />
    <HeaderEnd>{() => <button type="button">Sort</button>}</HeaderEnd>
    <HeaderEdge component={ResizeHandle} />
    <HeaderOverlay component={ReorderDropZone} />
    {() => 'Product'}
  </DataTableColumnHeader>
  <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>

When a header contains any slots, the label must be a single render-function child ({() => 'Product'}) rather than a plain string. The header needs a function to evaluate alongside the slots; a string child only works for plain headers without slots. See Cell and Header Renderers for the broader story on render functions.

Slot components are passed the column key, the column metadata, the column state, and the header element ref — enough to publish engine actions, render indicators, and position overlays without DOM queries. That ref is what lets a drop-indicator overlay measure the header it’s painting over, and what lets an edge-pinned handle compute its drag delta.

Where this shows up in the shadcn registry

Section titled “Where this shows up in the shadcn registry”

The opt-in column features ship slot components that drop straight into these positions:

  • ResizeHandle from @/components/ui/data-table/column-resize — mounts in HeaderEdge. See Column Resizing.
  • ReorderGrip and ReorderDropZone from @/components/ui/data-table/column-reorder — mount in HeaderStart and HeaderOverlay respectively. See Column Reordering.

Custom sort, filter, menu, and selection controls follow the same pattern: a component that takes the slot’s render params and returns whatever JSX you need.