Skip to content

Column Reorder Example

Each header carries a GripVertical icon at the start and a transparent drop zone across the rest. Drag a grip over another header to move the source column before or after the target.

The shadcn components import from @virtuoso.dev/data-table/column-reorder — consumers that never reorder columns don’t ship that reducer. The grip is a Lucide icon, sized and spaced like the other shadcn header affordances so it visually separates from the column title.

  • ReorderGrip — draggable header affordance
  • ReorderDropZone — header drop target
  • HeaderStart / HeaderOverlay — slot positions for the components
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader, HeaderOverlay, HeaderStart } from '@/components/ui/data-table'
import { ReorderDropZone, ReorderGrip } from '@/components/ui/data-table/column-reorder'
import { localModel } from '@virtuoso.dev/data-table'

import { products } from './data'

const model = localModel({ data: products })
const columns = [
  ['name', 'Product', 'font-medium'],
  ['category', 'Category', ''],
  ['status', 'Status', ''],
  ['region', 'Region', ''],
] as const

export default function App() {
  return (
    <DataTable className="rounded-xl" computeRowKey={({ data }) => data.id} model={model} style={{ height: 400 }}>
      {columns.map(([field, label, cellClassName]) => (
        <DataTableColumn field={field} key={field}>
          <DataTableColumnHeader>
            <HeaderStart component={ReorderGrip} />
            <HeaderOverlay component={ReorderDropZone} />
            {() => label}
          </DataTableColumnHeader>
          <DataTableCell className={cellClassName}>{({ cellValue }) => String(cellValue)}</DataTableCell>
        </DataTableColumn>
      ))}
    </DataTable>
  )
}

Drag-and-drop is the spreadsheet-style affordance. For accessible alternatives or deterministic test setup, publish reorderColumns$ from an external menu or button — the streams accept the same operations the drag UI emits.