Data Table

A sortable, searchable, filterable data table for TanStack Router with cursor pagination and URL state persistence.

ContentDataTable is the core data table component powering VitNode AdminCP lists. Every user interaction - page navigation, column sorting, search queries, and filters - writes directly to the URL query string, making table state fully bookmarkable and shareable.

Preview

NameEmailRoleStatus
John Doejon_doe@mail.comAdminActive
Jane Smithjane_smith@mail.comEditorInactive
Alice Johnsonalice_johnson@mail.comViewerActive
Bob Brownbob_brown@mail.comAdminInactive

Quick start

Render the table inside a DataTableNavigationProvider:

plugins/members/src/views/members-table.tsx
import type { ColumnDef } from '@vitnode/core/components/table/data-table-content'
import { ContentDataTable } from '@vitnode/core/components/table/content'
import { DataTableNavigationProvider } from '@vitnode/core/components/table/provider'

interface Member {
  id: number
  name: string
  email: string
  role: string
}

const columns: ColumnDef<Member>[] = [
  { accessorKey: 'name', header: 'Name', enableSorting: true },
  { accessorKey: 'email', header: 'Email' },
  { accessorKey: 'role', header: 'Role' },
]

interface MembersSearch {
  cursor?: string
}

interface MembersTableProps {
  data: Member[]
  navigate: (options: { search: MembersSearch }) => Promise<void>
  search: MembersSearch
}

export const MembersTable = ({ data, navigate, search }: MembersTableProps) => {
  return (
    <DataTableNavigationProvider navigate={navigate} search={search}>
      <ContentDataTable
        id="members-table"
        columns={columns}
        data={data}
        order={{
          defaultOrder: { column: 'name', order: 'asc' },
        }}
        search={{ placeholder: 'Search members...' }}
      />
    </DataTableNavigationProvider>
  )
}

Plugin Route Integration

Give an AdminCP route to the plugin, then pass its typed search and navigate props into the table. The table remains reusable and the host stays out of it.

plugins/members/src/pages/admin-members-page.tsx
import type { PluginRoutePageProps } from '@vitnode/core/routing'
import { definePluginRoute } from '@vitnode/core/routing'

import { MembersTable } from '../views/members-table'

interface MembersSearch {
  cursor?: string
}

export const route = definePluginRoute({
  parseSearch: (input) => {
    const search = input as Record<string, unknown>
    return {
      cursor: typeof search.cursor === 'string' ? search.cursor : undefined,
    }
  },
  load: async ({ search }) => await fetchMembers(search),
})

const AdminMembersPage = ({
  loaderData,
  navigate,
  search,
}: PluginRoutePageProps<Member[], MembersSearch>) => (
  <MembersTable data={loaderData} navigate={navigate} search={search} />
)

export default AdminMembersPage

Filters

Add static or asynchronous dropdown filters to the table toolbar:

<ContentDataTable
  id="members-table"
  columns={columns}
  data={data}
  filters={[
    {
      id: 'role',
      title: 'Role',
      options: [
        { label: 'Admin', value: 'admin' },
        { label: 'Member', value: 'member' },
      ],
    },
  ]}
/>

Bulk Selection & Actions

Enable row checkboxes and execute bulk operations with useDataTableSelection:

import { useDataTableSelection } from '@vitnode/core/components/table/hooks/use-data-table-selection'

export const BulkActionsToolbar = () => {
  const { selectedRows, resetSelection } = useDataTableSelection()

  if (!selectedRows.length) return null

  return (
    <div className="flex items-center gap-2 p-2 bg-muted rounded-md">
      <span>{selectedRows.length} selected</span>
      <button onClick={() => deleteSelected(selectedRows)}>Delete</button>
    </div>
  )
}

ContentDataTable Props

Prop

Type

Learn More