Sticky Columns Example
A product column pinned to the left, a status column pinned to the right, and a dozen metric columns in between to force horizontal scrolling — the only situation where sticky columns matter. The toolbar above the table flips the status column’s sticky side at runtime through the engine.
APIs used
Section titled “APIs used”sticky="left"/sticky="right"— declarative pinsetColumnSticky$— runtime sticky changes from external UI
import { Button } from '@/components/ui/button'
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { localModel, columns$, setColumnSticky$, useEngineRef, useRemoteCellValue, useRemotePublisher } from '@virtuoso.dev/data-table'
import { metricFields, products } from './data'
import type { ColumnInfo } from '@virtuoso.dev/data-table'
function findColumnKey(columns: Map<string, ColumnInfo> | undefined, field: string) {
return columns ? [...columns].find(([, info]) => info.field === field)?.[0] : undefined
}
const model = localModel({ data: products })
export default function App() {
const engineRef = useEngineRef()
const columns = useRemoteCellValue(columns$, engineRef)
const setColumnSticky = useRemotePublisher(setColumnSticky$, engineRef)
const statusKey = findColumnKey(columns, 'status')
return (
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<Button
disabled={!statusKey}
onClick={() => {
if (statusKey) {
setColumnSticky({ key: statusKey, sticky: 'left' })
}
}}
variant="outline"
>
Pin status left
</Button>
<Button
disabled={!statusKey}
onClick={() => {
if (statusKey) {
setColumnSticky({ key: statusKey, sticky: 'right' })
}
}}
variant="outline"
>
Pin status right
</Button>
<Button
disabled={!statusKey}
onClick={() => {
if (statusKey) {
setColumnSticky({ key: statusKey, sticky: undefined })
}
}}
variant="outline"
>
Clear sticky
</Button>
</div>
<DataTable className="rounded-xl" model={model} engineRef={engineRef} style={{ height: 400 }}>
<DataTableColumn field="name" sticky="left">
<DataTableColumnHeader className="min-w-[180px]">Product</DataTableColumnHeader>
<DataTableCell className="font-medium">{({ row }) => row.data.name}</DataTableCell>
</DataTableColumn>
{metricFields.map((field) => (
<DataTableColumn field={field} key={field}>
<DataTableColumnHeader className="min-w-[140px]">{field}</DataTableColumnHeader>
<DataTableCell className="tabular-nums">{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
))}
<DataTableColumn field="status" sticky="right">
<DataTableColumnHeader className="min-w-[120px]">Status</DataTableColumnHeader>
<DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
</DataTable>
</div>
)
}Each pinned column takes width from the scrollable area. Past two or three sticky columns, horizontal scrolling stops being useful — you’ve spent your row width on pinned content and the remaining viewport can no longer fit anything meaningful.
The runtime toggle uses setColumnSticky$ against the same column key the table assigned during declaration. Look up the key by field with columns$ once at the call site instead of holding it in component state; the engine is the source of truth.