Skip to content

Column Visibility Example

A toolbar reads columns$ and columnVisibilityState$ through an engineRef and publishes setColumnVisibility$ and resetColumnVisibility$. The internalId column starts hidden via visible={false} and can be toggled back on from the toolbar.

  • columns$ — column declarations from the engine
  • columnVisibilityState$ — current visibility overrides
  • setColumnVisibility$ / resetColumnVisibility$ — toggle and reset
import { Button } from '@/components/ui/button'
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { localModel, columns$, useEngineRef, useRemoteCellValue, useRemotePublisher } from '@virtuoso.dev/data-table'
import { columnVisibilityState$, resetColumnVisibility$, setColumnVisibility$ } from '@virtuoso.dev/data-table/column-visibility'

import { products } from './data'

const model = localModel({ data: products })

export default function App() {
  const engineRef = useEngineRef()
  const columns = useRemoteCellValue(columns$, engineRef)
  const visibility = useRemoteCellValue(columnVisibilityState$, engineRef)
  const setColumnVisibility = useRemotePublisher(setColumnVisibility$, engineRef)
  const resetColumnVisibility = useRemotePublisher(resetColumnVisibility$, engineRef)

  return (
    <div className="space-y-4">
      <div className="flex flex-wrap gap-2">
        {[...(columns ?? new Map())].map(([key, column]) => {
          const visible = visibility?.get(key) ?? column.visible !== false
          return (
            <Button key={key} onClick={() => setColumnVisibility({ key, visible: !visible })} variant={visible ? 'secondary' : 'outline'}>
              {visible ? 'Hide' : 'Show'} {column.field}
            </Button>
          )
        })}
        <Button onClick={() => resetColumnVisibility()} variant="outline">
          Reset
        </Button>
      </div>

      <DataTable className="rounded-xl" model={model} engineRef={engineRef} style={{ height: 400 }}>
        <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="status">
          <DataTableColumnHeader>Status</DataTableColumnHeader>
          <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
        </DataTableColumn>
        <DataTableColumn field="region">
          <DataTableColumnHeader>Region</DataTableColumnHeader>
          <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
        </DataTableColumn>
        <DataTableColumn field="internalId" visible={false}>
          <DataTableColumnHeader>Internal ID</DataTableColumnHeader>
          <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
        </DataTableColumn>
      </DataTable>
    </div>
  )
}

Persisted visibility is keyed by field, not by the generated runtime column key. Renaming a field invalidates its saved visibility — see Field names are the persistence contract for the migration rule.