DataGrid

DataGrid is a composable data surface for product-grade tabular experiences. It combines TanStack Table behavior with opinionated UI composition for search, filters, pagination, cards/table view switching, and exports.

Mental model

DataGrid owns table interaction state and rendering orchestration.

You provide:

  • row data and column definitions
  • optional custom renderers (renderCard, renderSubComponent)
  • optional server query integration (onQueryChange)

Features

  • client mode and server mode query orchestration
  • global fuzzy search and per-column filtering
  • column sorting and visibility control
  • pagination with configurable page sizes
  • row expansion with nested content rendering
  • switchable table and card presentations
  • command-based controls for filters/view/export

Quick start

type User = {
  id: string;
  name: string;
  email: string;
  status: 'active' | 'trial' | 'paused';
};

const columns: ColumnDef<User>[] = [
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'email', header: 'Email' },
  { accessorKey: 'status', header: 'Status', enableColumnFilter: true },
];

<DataGrid data={users} columns={columns} />;

Query modes

Client mode (default)

Filtering, sorting, and pagination are computed locally by TanStack Table. Use this when your dataset is already in memory.

Server mode

Set mode='server' and subscribe to onQueryChange. Use returned query state to call APIs and push back new data.

<DataGrid
  mode='server'
  data={rows}
  columns={columns}
  pageCount={pageCount}
  totalRows={totalRows}
  onQueryChange={query => {
    // query: { pageIndex, pageSize, sorting, globalFilter, columnFilters }
    refetch(query);
  }}
/>

Toolbar extensibility

Use toolbarStart and toolbarEnd to inject additional controls. Each slot accepts either static nodes or a function that receives toolbar context (table, query, filter actions, loading state).

<DataGrid
  ...
  toolbarEnd={({ clearFilters }) => (
    <Button size='sm' variant='outline' onClick={clearFilters}>
      Clear filters
    </Button>
  )}
/>

Legacy render props (renderToolbarStart, renderToolbarEnd) are still supported for backward compatibility.

Props reference

PropTypeDefaultNotes
dataTData[]requiredRow dataset to render.
columnsColumnDef<TData, unknown>[]requiredTanStack column definitions.
mode'client' | 'server''client'Query execution strategy.
totalRowsnumber-Total row count in server mode.
pageCountnumber-Explicit page count override in server mode.
loadingbooleanfalseEnables loading placeholders.
classNamestring-Root grid wrapper classes.
tableClassNamestring-Table view wrapper classes.
cardClassNamestring-Cards grid wrapper classes.
emptyMessagestring'No results found.'Empty-state copy.
initialView'table' | 'cards''table'Uncontrolled initial view.
view'table' | 'cards'-Controlled view override.
onViewChange(view) => void-Called when view changes.
enableGlobalFilterbooleantrueGlobal search enablement.
enableColumnFiltersbooleantruePer-column filters enablement.
enableColumnVisibilitybooleantrueColumn visibility controls enablement.
enableViewModesbooleantrueTable/cards toggle enablement.
exportOptionsDataGridExportOptions{ enabled: true, fileName: 'data-grid', mode: 'visible', prettyJson: true }Export configuration for toolbar command.
toolbarClassNamestring-Toolbar wrapper classes.
toolbarStartDataGridToolbarSlot<TData>-Start slot node or render function.
toolbarEndDataGridToolbarSlot<TData>-End slot node or render function.
renderToolbarStart(context) => ReactNode-Legacy start slot callback.
renderToolbarEnd(context) => ReactNode-Legacy end slot callback.
pageSizeOptionsnumber[][10, 20, 30, 50]Pagination page size choices.
initialStatePartial<DataGridQueryState>{ pageIndex: 0, pageSize: 10, sorting: [], globalFilter: '', columnFilters: [] }Initial query state values.
getRowId(originalRow, index, parent?) => string-Custom row id strategy.
getSubRows(originalRow, index) => TData[] | undefined-Sub-row resolver for expandable trees.
renderCard(context) => ReactNode-Custom cards mode renderer.
renderSubComponent(context) => ReactNode-Expanded row content renderer.
onQueryChange(query: DataGridQueryState) => void-Query snapshot callback for server orchestration.

Filter model

Column filter behavior is configured in column.columnDef.meta.filter.

Supported filter kinds:

  • multi-select
  • number-range
  • date-range

Example:

{
  accessorKey: 'usage',
  header: 'Usage',
  enableColumnFilter: true,
  filterFn: 'numberRange',
  meta: {
    filter: {
      kind: 'number-range',
      label: 'Usage',
      min: 0,
      max: 100,
      step: 1,
    },
  },
}

View customization

  • renderCard customizes cards mode item rendering.
  • renderSubComponent renders expandable table row details.

If no renderCard is provided, DataGrid renders a default generic card from visible cell values.

Export behavior

Exports are controlled through exportOptions:

  • enabled: show/hide export controls
  • fileName: output base file name
  • mode: visible (default) or original
  • prettyJson: JSON pretty-print formatting

visible mode exports current visible columns in their active order.

  • Use mode='server' for large datasets.
  • Provide stable getRowId for non-id-based data.
  • Keep columnDef.meta.filter declarative; avoid ad-hoc filter state outside grid query state.
  • Prefer renderCard for domain-specific cards rather than overloading cell renderers for cards mode.

Accessibility notes

  • View controls and filter command menu are keyboard accessible.
  • Sortable headers expose sort state semantics.
  • Row expansion toggles include explicit labels.

See also

  • Table for low-level table primitives.
  • ARCHITECTURE.md in this folder for maintainer-focused internals.

Preview

Usage

'use client';

import { formatTimestamp, toTitleCase } from '@branditdev/utils';
import { type ColumnDef } from '@tanstack/react-table';
import {
  BarChartIcon,
  BriefcaseIcon,
  CalendarIcon,
  GlobeIcon,
  MailIcon,
  PlusIcon,
  TrendingUp,
  UserIcon,
} from 'lucide-react';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts';

import { Avatar, AvatarFallback, Button, Card, CardContent, CardFooter, CardHeader, CardTitle, Chart, Text, DataGrid } from '@branditdev/ui';

type TeamRow = {
  id: string;
  name: string;
  email: string;
  role: 'Engineer' | 'Designer' | 'Product' | 'Support';
  region: 'US' | 'EU' | 'APAC';
  status: 'active' | 'trial' | 'paused';
  usage: number;
  dateStarted: Date;
  subRows?: TeamRow[];
};

function randomDateInLastTenYears() {
  // Set the end date to today
  const endDate = new Date();

  // Set the start date to exactly five years ago
  const startDate = new Date();
  startDate.setFullYear(endDate.getFullYear() - 10);

  // Get the time values (milliseconds since Jan 1, 1970 UTC)
  const startTime = startDate.getTime();
  const endTime = endDate.getTime();

  // Calculate a random time value between the start and end times
  // Math.random() returns a value between 0 (inclusive) and 1 (exclusive)
  const randomTime = startTime + Math.random() * (endTime - startTime);

  // Create a new Date object using the random time
  return new Date(randomTime);
}

const StatusBadge = ({ status }: { status: TeamRow['status'] }) => {
  const palette = {
    active: 'bg-emerald-100 text-emerald-800',
    trial: 'bg-amber-100 text-amber-800',
    paused: 'bg-slate-200 text-slate-700',
  };
  return (
    <span
      className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${palette[status]}`}
    >
      {status}
    </span>
  );
};

const firstNames = [
  'Noah',
  'Emma',
  'Liam',
  'Olivia',
  'Mason',
  'Ava',
  'Ethan',
  'Sophia',
  'Lucas',
  'Isabella',
];

const lastNames = [
  'Carter',
  'Morgan',
  'Nguyen',
  'Kim',
  'Lopez',
  'Bennett',
  'Singh',
  'Campbell',
  'Murphy',
  'Reed',
];

const roles: TeamRow['role'][] = ['Engineer', 'Designer', 'Product', 'Support'];
const regions: TeamRow['region'][] = ['US', 'EU', 'APAC'];
const statuses: TeamRow['status'][] = ['active', 'trial', 'paused'];

function cycleValue<T>(values: T[], index: number): T {
  return values[index % values.length]!;
}

const generatedRows: TeamRow[] = Array.from({ length: 100 }, (_, index) => {
  const id = index + 6;
  const firstName = cycleValue(firstNames, index);
  const lastName = cycleValue(lastNames, Math.floor(index / firstNames.length));
  const name = `${firstName} ${lastName}`;
  const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}${id}@marketfuel.com`;

  return {
    id: `u${id}`,
    name,
    email,
    role: cycleValue(roles, index),
    region: cycleValue(regions, index),
    status: cycleValue(statuses, index),
    usage: 35 + ((index * 7) % 66),
    dateStarted: randomDateInLastTenYears(),
  };
});

const columns: ColumnDef<TeamRow>[] = [
  {
    meta: {
      icon: UserIcon,
    },
    accessorKey: 'name',
    header: 'Name',
    enableColumnFilter: true,
  },
  {
    meta: {
      icon: CalendarIcon,
      filter: {
        kind: 'date-range',
        label: 'Date Started',
      },
    },
    accessorKey: 'dateStarted',
    header: 'Date Started',
    enableColumnFilter: true,
    filterFn: 'dateRange',
    cell: info => {
      const value = info.getValue<Date>();
      const formatted = formatTimestamp(value.toISOString(), {
        format: {
          year: 'numeric',
          month: '2-digit',
          day: '2-digit',
          hour: undefined,
          minute: undefined,
        },
      });

      return <Text variant='body-sm'>{formatted}</Text>;
    },
  },
  {
    meta: {
      icon: MailIcon,
    },
    accessorKey: 'email',
    header: 'Email',
    enableColumnFilter: true,
  },
  {
    meta: {
      icon: BriefcaseIcon,
      filterLabel: 'Role',
    },
    accessorKey: 'role',
    header: 'Role',
    enableColumnFilter: true,
  },
  {
    meta: {
      icon: GlobeIcon,
      filterLabel: 'Region',
    },
    accessorKey: 'region',
    header: 'Region',
    enableColumnFilter: true,
  },
  {
    meta: {
      icon: TrendingUp,
      filterLabel: 'Status',
    },
    accessorKey: 'status',
    header: 'Status',
    enableColumnFilter: true,
    cell: info => {
      const value = info.getValue<TeamRow['status']>();
      return <StatusBadge status={value} />;
    },
  },
  {
    accessorKey: 'usage',
    header: 'Usage',
    enableColumnFilter: true,
    filterFn: 'numberRange',
    meta: {
      cardLabel: 'Usage Score',
      icon: BarChartIcon,
      filter: {
        kind: 'number-range',
        label: 'Usage',
        min: 0,
        max: 100,
        step: 1,
      },
    },
    cell: info => `${info.getValue<number>()}%`,
  },
];

export function DataGridDemo() {
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setTimeout(() => {
      setLoading(false);
    }, 3000);
  }, []);

  return (
    <div className='space-y-3'>
      <DataGrid
        toolbarEnd={
          <Button
            variant='outline'
            size='sm'
            // eslint-disable-next-line no-console
            onClick={() => console.info('Create User action clicked')}
          >
            <PlusIcon className='size-4' />
            <span className='hidden md:inline'>Create User</span>
          </Button>
        }
        loading={loading}
        data={generatedRows}
        columns={columns}
        getSubRows={row => row.subRows}
        pageSizeOptions={[10, 20, 30, 50]}
        renderSubComponent={({ row }) => (
          <div className='text-muted-foreground text-xs'>
            Expanded details for {row.original.name}
          </div>
        )}
        renderCard={({ row }) => (
          <Card key={row.id} className='overflow-hidden'>
            <CardHeader>
              <div className='flex items-center gap-3'>
                <Avatar>
                  <AvatarFallback>
                    {row.original.name
                      .split(' ')
                      .map(part => part[0])
                      .join('')
                      .slice(0, 2)}
                  </AvatarFallback>
                </Avatar>
                <CardTitle>{row.original.name}</CardTitle>
              </div>
            </CardHeader>
            <CardContent className='space-y-4'>
              <div className='grid grid-cols-2 gap-3 text-sm'>
                <div className='bg-muted/40 rounded-md p-3'>
                  <Text
                    variant='body-sm'
                    className='text-muted-foreground flex items-center gap-1'
                  >
                    <TrendingUp className='size-3.5' /> Status
                  </Text>
                  <Text variant='body-sm' className='mt-1 font-medium'>
                    {toTitleCase(row.original.status)}
                  </Text>
                </div>
                <div className='bg-muted/40 rounded-md p-3'>
                  <Text
                    variant='body-sm'
                    className='text-muted-foreground flex items-center gap-1'
                  >
                    <CalendarIcon className='size-3.5' /> Started
                  </Text>
                  <Text variant='body-sm' className='mt-1 font-medium'>
                    {formatTimestamp(row.original.dateStarted.toISOString(), {
                      format: {
                        year: 'numeric',
                        month: '2-digit',
                        day: '2-digit',
                        hour: undefined,
                        minute: undefined,
                      },
                    })}
                  </Text>
                </div>
                <div className='bg-muted/40 rounded-md p-3'>
                  <Text
                    variant='body-sm'
                    className='text-muted-foreground flex items-center gap-1'
                  >
                    <BriefcaseIcon className='size-3.5' /> Role
                  </Text>
                  <Text variant='body-sm' className='mt-1 font-medium'>
                    {row.original.role}
                  </Text>
                </div>
                <div className='bg-muted/40 rounded-md p-3'>
                  <Text
                    variant='body-sm'
                    className='text-muted-foreground flex items-center gap-1'
                  >
                    <GlobeIcon className='size-3.5' /> Region
                  </Text>
                  <Text variant='body-sm' className='mt-1 font-medium'>
                    {row.original.region}
                  </Text>
                </div>
                <div className='bg-muted/40 rounded-md p-3 col-span-2'>
                  <Text
                    variant='body-sm'
                    className='text-muted-foreground flex items-center gap-1'
                  >
                    <BarChartIcon className='size-3.5' /> Usage
                  </Text>

                  <Chart className='h-24 p-2 mt-1 font-medium'>
                    <ResponsiveContainer width='100%' height='100%'>
                      <PieChart>
                        <Pie
                          data={[
                            { name: 'used', value: row.original.usage },
                            {
                              name: 'remaining',
                              value: 100 - row.original.usage,
                            },
                          ]}
                          dataKey='value'
                          cx='50%'
                          cy='50%'
                          innerRadius={24}
                          outerRadius={36}
                          stroke='none'
                        >
                          <Cell fill='var(--chart-5)' />
                          <Cell fill='var(--chart-2)' />
                        </Pie>
                        <text
                          x='50%'
                          y='50%'
                          textAnchor='middle'
                          dominantBaseline='middle'
                          className='fill-foreground text-xs font-semibold'
                        >
                          {row.original.usage}%
                        </text>
                      </PieChart>
                    </ResponsiveContainer>
                  </Chart>
                </div>
              </div>
            </CardContent>
            <CardFooter className='border-t pt-4 flex items-center justify-center space-x-2'>
              <Button
                variant='ghost'
                size='sm'
                className='flex-1'
                onClick={() =>
                  window.open(`mailto:${row.original.email}`, '_blank')
                }
              >
                <MailIcon className='size-4' />
                Contact
              </Button>
              <Button
                size='sm'
                className='flex-1'
                onClick={() =>
                  // eslint-disable-next-line no-console
                  console.info(`Open profile for ${row.original.id}`)
                }
              >
                <UserIcon className='size-4' />
                View profile
              </Button>
            </CardFooter>
          </Card>
        )}
      />
    </div>
  );
}

API

DataGrid<TData>(props): Element

Defined in: components/data-grid/data-grid.tsx:53

Robust data grid with table/card views and client/server query modes.

Type Parameters

TData

TData extends unknown

Parameters

NameTypeOptionalDefaultDescription
propsDataGridProps<TData>no-Data grid configuration and data source options.

Returns

Element

A composable data visualization surface.

Source

'use client';

import type { DragEndEvent } from '@dnd-kit/core';
import {
  closestCenter,
  DndContext,
  KeyboardSensor,
  MouseSensor,
  TouchSensor,
  useSensor,
  useSensors,
} from '@dnd-kit/core';
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';
import { arrayMove } from '@dnd-kit/sortable';
import {
  type RowData,
  getCoreRowModel,
  getExpandedRowModel,
  getFacetedRowModel,
  getFacetedUniqueValues,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from '@tanstack/react-table';
import * as React from 'react';

import { cn, DataGridCards, DataTablePagination, dataGridActions, dataGridReducer, DataGridTable, DataGridToolbar } from '@branditdev/ui';
import {
  type DataGridProps,
  type DataGridQueryState,
  type DataGridToolbarContext,
  type DataGridViewMode,
  defaultState,
  dateRangeFilter as dateRange,
  fuzzyFilter as fuzzy,
  numberRangeFilter as numberRange,
  resolveToolbarSlot,
} from './data-grid-utils';

export function DataGrid<TData extends RowData>({
  data,
  columns,
  mode = defaultState.mode,
  totalRows,
  pageCount,
  loading = defaultState.loadingState,
  className,
  tableClassName,
  cardClassName,
  emptyMessage = defaultState.emptyMessage,
  initialView = defaultState.initialView,
  view,
  onViewChange,
  enableGlobalFilter = defaultState.enableGlobalFilter,
  enableColumnFilters = defaultState.enableColumnFilters,
  enableColumnVisibility = defaultState.enableColumnVisibility,
  enableViewModes = defaultState.enableViewModes,
  exportOptions = defaultState.exportOptions,
  toolbarClassName,
  toolbarStart,
  toolbarEnd,
  renderToolbarStart,
  renderToolbarEnd,
  pageSizeOptions = defaultState.pageSizeOptions,
  initialState = defaultState.initialState,
  getRowId,
  getSubRows,
  renderCard,
  renderSubComponent,
  onQueryChange,
}: DataGridProps<TData>) {
  const [state, dispatch] = React.useReducer(dataGridReducer, {
    sorting: initialState?.sorting ?? defaultState.initialState.sorting,
    columnFilters:
      initialState?.columnFilters ?? defaultState.initialState.columnFilters,
    globalFilter:
      initialState?.globalFilter ?? defaultState.initialState.globalFilter,
    pagination: {
      pageIndex: initialState?.pageIndex ?? defaultState.initialState.pageIndex,
      pageSize: initialState?.pageSize ?? pageSizeOptions[0] ?? 10,
    },
    columnVisibility: {},
    expanded: {},
    view: initialView ?? defaultState.initialView,
    columnOrder: columns
      .map(column => {
        if (column.id) return column.id;
        if ('accessorKey' in column && column.accessorKey)
          return String(column.accessorKey);

        return undefined;
      })
      .filter((id): id is string => Boolean(id)),
  });

  const {
    setSorting,
    setColumnFilters,
    setGlobalFilter,
    setPagination,
    setColumnVisibility,
    setExpanded,
    setView,
    setColumnOrder,
  } = React.useMemo(
    () => ({
      setSorting: dataGridActions.setSorting(dispatch),
      setColumnFilters: dataGridActions.setColumnFilters(dispatch),
      setGlobalFilter: dataGridActions.setGlobalFilter(dispatch),
      setPagination: dataGridActions.setPagination(dispatch),
      setColumnVisibility: dataGridActions.setColumnVisibility(dispatch),
      setExpanded: dataGridActions.setExpanded(dispatch),
      setView: dataGridActions.setView(dispatch),
      setColumnOrder: dataGridActions.setColumnOrder(dispatch),
    }),
    [dispatch]
  );

  const activeView = view ?? state.view;
  const setActiveView = React.useCallback(
    (nextView: DataGridViewMode) => {
      onViewChange?.(nextView);
      if (!view) setView(nextView);
    },
    [onViewChange, view, setView]
  );

  const table = useReactTable({
    data,
    columns,
    filterFns: { fuzzy, numberRange, dateRange },
    globalFilterFn: 'fuzzy',
    defaultColumn: {
      filterFn: 'fuzzy',
      minSize: 60,
      size: 180,
      maxSize: 800,
    },
    state: {
      sorting: state.sorting,
      columnFilters: state.columnFilters,
      globalFilter: state.globalFilter ?? '',
      pagination: state.pagination,
      columnVisibility: state.columnVisibility,
      expanded: state.expanded,
      columnOrder: state.columnOrder,
    },
    getRowId,
    getSubRows,
    manualPagination: mode === 'server',
    manualSorting: mode === 'server',
    manualFiltering: mode === 'server',
    pageCount:
      mode === 'server'
        ? (pageCount ??
          (typeof totalRows === 'number'
            ? Math.ceil(totalRows / state.pagination.pageSize)
            : -1))
        : undefined,
    enableColumnResizing: true,
    columnResizeMode: 'onChange',
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onGlobalFilterChange: setGlobalFilter,
    onPaginationChange: setPagination,
    onColumnVisibilityChange: setColumnVisibility,
    onExpandedChange: setExpanded,
    onColumnOrderChange: setColumnOrder,
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: mode === 'client' ? getFilteredRowModel() : undefined,
    getFacetedRowModel: mode === 'client' ? getFacetedRowModel() : undefined,
    getFacetedUniqueValues:
      mode === 'client' ? getFacetedUniqueValues() : undefined,
    getSortedRowModel: mode === 'client' ? getSortedRowModel() : undefined,
    getPaginationRowModel:
      mode === 'client' ? getPaginationRowModel() : undefined,
    getExpandedRowModel: getExpandedRowModel(),
  });

  React.useEffect(() => {
    onQueryChange?.({
      pageIndex: state.pagination.pageIndex,
      pageSize: state.pagination.pageSize,
      sorting: state.sorting,
      globalFilter: state.globalFilter ?? '',
      columnFilters: state.columnFilters,
    });
  }, [
    state.globalFilter,
    onQueryChange,
    state.pagination,
    state.sorting,
    state.columnFilters,
  ]);

  const loadingCardCount = defaultState.loadingCardCount;
  const queryState: DataGridQueryState = {
    pageIndex: state.pagination.pageIndex,
    pageSize: state.pagination.pageSize,
    sorting: state.sorting,
    globalFilter: state.globalFilter ?? '',
    columnFilters: state.columnFilters,
  };
  const clearGlobalFilter = () => setGlobalFilter('');
  const clearFilters = () => {
    setGlobalFilter('');
    setColumnFilters([]);
  };
  const toolbarContext: DataGridToolbarContext<TData> = {
    table,
    query: queryState,
    activeView,
    setActiveView,
    globalFilter: state.globalFilter ?? '',
    setGlobalFilter,
    clearGlobalFilter,
    clearFilters,
    loading,
  };
  const toolbarStartSlot = resolveToolbarSlot(toolbarContext, toolbarStart);
  const toolbarEndSlot = resolveToolbarSlot(toolbarContext, toolbarEnd);
  const legacyToolbarStart = renderToolbarStart?.(toolbarContext);
  const legacyToolbarEnd = renderToolbarEnd?.(toolbarContext);
  const toolbarStartContent = (
    <>
      {toolbarStartSlot}
      {legacyToolbarStart}
    </>
  );
  const toolbarEndContent = (
    <>
      {toolbarEndSlot}
      {legacyToolbarEnd}
    </>
  );

  React.useEffect(() => {
    const nextIds = table.getAllLeafColumns().map(column => column.id);

    setColumnOrder(prev => {
      const retained = prev.filter(id => nextIds.includes(id));
      const additions = nextIds.filter(id => !retained.includes(id));
      const merged = [...retained, ...additions];

      if (
        merged.length === prev.length &&
        merged.every((id, index) => id === prev[index])
      )
        return prev;

      return merged;
    });
  }, [columns, setColumnOrder, table]);

  const dndContextId = React.useId();

  const handleDragEnd = (event: DragEndEvent) => {
    const { active, over } = event;
    if (active && over && active.id !== over.id) {
      const updateOrder = () => {
        const oldIndex = state.columnOrder.indexOf(active.id as string);
        const newIndex = state.columnOrder.indexOf(over.id as string);
        if (oldIndex < 0 || newIndex < 0) return state.columnOrder;

        return arrayMove(state.columnOrder, oldIndex, newIndex); //this is just a splice util
      };

      setColumnOrder(updateOrder());
    }
  };

  const sensors = useSensors(
    useSensor(MouseSensor, {}),
    useSensor(TouchSensor, {}),
    useSensor(KeyboardSensor, {})
  );

  return (
    <DndContext
      id={dndContextId}
      collisionDetection={closestCenter}
      modifiers={[restrictToHorizontalAxis]}
      onDragEnd={handleDragEnd}
      sensors={sensors}
    >
      <div className={cn('space-y-4', className)}>
        <DataGridToolbar
          toolbarClassName={toolbarClassName}
          searchControl={
            <DataGridToolbar.SearchControl
              enableGlobalFilter={enableGlobalFilter}
              globalFilter={state.globalFilter ?? ''}
              setGlobalFilter={setGlobalFilter}
              clearGlobalFilter={clearGlobalFilter}
            />
          }
          viewToggle={
            <DataGridToolbar.ViewToggle
              enableViewModes={enableViewModes}
              view={activeView}
              setActiveView={setActiveView}
            />
          }
          filtersCommand={
            <DataGridToolbar.FiltersCommand
              enableColumnFilters={enableColumnFilters}
              exportOptions={exportOptions}
              enableColumnVisibility={enableColumnVisibility}
              table={table}
              activeView={activeView}
              setActiveView={setActiveView}
              enableViewModes={enableViewModes}
            />
          }
          toolbarStartContent={toolbarStartContent}
          toolbarEndContent={toolbarEndContent}
        />
        {activeView === 'table' ? (
          <DataGridTable tableClassName={tableClassName}>
            <DataGridTable.Header
              enableColumnFilters={enableColumnFilters}
              table={table}
            />
            <DataGridTable.Body
              loading={loading}
              emptyMessage={emptyMessage}
              table={table}
              pagination={state.pagination}
              renderSubComponent={renderSubComponent}
            />
          </DataGridTable>
        ) : (
          <DataGridCards cardClassName={cardClassName}>
            <DataGridCards.Cards
              loading={loading}
              loadingCardCount={loadingCardCount}
              emptyMessage={emptyMessage}
              table={table}
              renderCard={renderCard}
            />
          </DataGridCards>
        )}
        <DataTablePagination table={table} pageSizeOptions={pageSizeOptions} />
      </div>
    </DndContext>
  );
}