Docs
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
Table API Reference
Column API Reference
Row API Reference
Cell API Reference
Header API Reference
Features API Reference
Static Functions API Reference
Legacy API Reference

React Example: Kitchen Sink Mantine

'use client'

import * as React from 'react'
import { TanStackDevtools } from '@tanstack/react-devtools'
import * as ReactDOM from 'react-dom/client'
import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer'
import {
  ActionIcon,
  Box,
  Button,
  Container,
  Group,
  MantineProvider,
  Table as MantineTable,
  Menu,
  Paper,
  Stack,
  TextInput,
  Tooltip,
  useComputedColorScheme,
  useMantineColorScheme,
} from '@mantine/core'
import '@mantine/core/styles.css'
import {
  IconDeviceDesktop,
  IconMoon,
  IconSearch,
  IconSun,
} from '@tabler/icons-react'
import {
  tableDevtoolsPlugin,
  useTanStackTableDevtools,
} from '@tanstack/react-table-devtools'
import type {
  Cell,
  Column,
  Header,
  HeaderGroup,
  Row,
} from '@tanstack/react-table'
import type { ExtendedColumnFilter } from '@/types'
import type { Person } from '@/lib/make-data'
import type { features } from '@/hooks/features'
import { makeData } from '@/lib/make-data'
import { useAppTable } from '@/hooks/table'
import { columns } from '@/columns'
import './styles/globals.css'

type AppColumn = Column<typeof features, Person, any>
type AppHeader = Header<typeof features, Person, any>
type AppRow = Row<typeof features, Person>
type AppCell = Cell<typeof features, Person, any>
type AppHeaderGroup = HeaderGroup<typeof features, Person>

function getCommonPinningStyles(
  column: AppColumn,
  isSelected = false,
): React.CSSProperties {
  const isPinned = column.getIsPinned()
  const isLastLeftPinnedColumn =
    isPinned === 'start' && column.getIsLastColumn('start')
  const isFirstRightPinnedColumn =
    isPinned === 'end' && column.getIsFirstColumn('end')

  return {
    boxShadow: isLastLeftPinnedColumn
      ? '-4px 0 4px -4px var(--mantine-color-default-border) inset'
      : isFirstRightPinnedColumn
        ? '4px 0 4px -4px var(--mantine-color-default-border) inset'
        : undefined,
    insetInlineStart:
      isPinned === 'start' ? `${column.getStart('start')}px` : undefined,
    insetInlineEnd:
      isPinned === 'end' ? `${column.getAfter('end')}px` : undefined,
    position: isPinned ? 'sticky' : 'relative',
    borderRight: isLastLeftPinnedColumn
      ? '1px solid var(--mantine-color-default-border)'
      : undefined,
    borderLeft: isFirstRightPinnedColumn
      ? '1px solid var(--mantine-color-default-border)'
      : undefined,
    background: isSelected
      ? 'var(--mantine-color-blue-light)'
      : isPinned
        ? 'var(--mantine-color-body)'
        : undefined,
    zIndex: isPinned ? 2 : 0,
  }
}

function ModeMenu() {
  const { colorScheme, setColorScheme } = useMantineColorScheme()
  const computedColorScheme = useComputedColorScheme('light')
  const icon =
    computedColorScheme === 'dark' ? (
      <IconMoon size={18} />
    ) : (
      <IconSun size={18} />
    )

  return (
    <Menu shadow="md" width={150}>
      <Menu.Target>
        <Tooltip label="Theme">
          <ActionIcon variant="subtle" aria-label="Theme">
            {icon}
          </ActionIcon>
        </Tooltip>
      </Menu.Target>
      <Menu.Dropdown>
        {(
          [
            { value: 'light', label: 'Light', icon: <IconSun size={16} /> },
            { value: 'dark', label: 'Dark', icon: <IconMoon size={16} /> },
            {
              value: 'auto',
              label: 'Auto',
              icon: <IconDeviceDesktop size={16} />,
            },
          ] satisfies Array<{
            value: 'light' | 'dark' | 'auto'
            label: string
            icon: React.ReactNode
          }>
        ).map((item) => (
          <Menu.Item
            key={item.value}
            leftSection={item.icon}
            color={colorScheme === item.value ? 'blue' : undefined}
            onClick={() => setColorScheme(item.value)}
          >
            {item.label}
          </Menu.Item>
        ))}
      </Menu.Dropdown>
    </Menu>
  )
}

function DebouncedTextInput({
  value: initialValue,
  onChange,
  debounce = 300,
  ...props
}: {
  value: string | number
  onChange: (value: string | number) => void
  debounce?: number
} & Omit<React.ComponentProps<typeof TextInput>, 'onChange'>) {
  const [value, setValue] = React.useState(initialValue)

  React.useEffect(() => {
    setValue(initialValue)
  }, [initialValue])

  const debouncedOnChange = useDebouncedCallback(onChange, { wait: debounce })

  return (
    <TextInput
      {...props}
      value={value}
      onChange={(event) => {
        setValue(event.currentTarget.value)
        debouncedOnChange(event.currentTarget.value)
      }}
    />
  )
}

function App() {
  const [columnFilters, setColumnFilters] = React.useState<
    Array<ExtendedColumnFilter>
  >([])
  const [globalFilter, setGlobalFilter] = React.useState('')
  const [data, setData] = React.useState(() => makeData(1_000))

  const refreshData = () => setData(makeData(1_000))
  const stressTest = () => setData(makeData(1_000_000))

  const table = useAppTable(
    {
      key: 'kitchen-sink-mantine',
      columns,
      data,
      debugTable: true,
      state: {
        columnFilters,
        globalFilter,
      },
      onColumnFiltersChange: setColumnFilters,
      onGlobalFilterChange: setGlobalFilter,
      initialState: {
        columnPinning: { start: ['select'], end: ['actions'] },
        columnOrder: columns.map((c) => c.id ?? ''),
      },
    },
    (state) => state, // default selector
  )

  useTanStackTableDevtools(table)

  return (
    <table.AppTable>
      <Container fluid py="md">
        <Stack gap="md">
          <Paper withBorder p="sm">
            <Group justify="flex-end" gap="xs">
              <ModeMenu />
              <Button variant="outline" size="sm" onClick={refreshData}>
                Regenerate Data
              </Button>
              <Button variant="outline" size="sm" onClick={stressTest}>
                Stress Test (1M rows)
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() =>
                  console.info(
                    'table.getSelectedRowModel().flatRows',
                    table.getSelectedRowModel().flatRows,
                  )
                }
              >
                Log Selected Rows
              </Button>
            </Group>
          </Paper>

          <Group align="center" gap="xs">
            <DebouncedTextInput
              value={globalFilter}
              onChange={(value) => setGlobalFilter(String(value))}
              placeholder="Search all columns..."
              leftSection={<IconSearch size={16} />}
              w={{ base: '100%', md: 360 }}
            />
            <table.FilterList />
            <table.SortList />
            <table.ViewOptions />
          </Group>

          <Paper withBorder>
            <MantineTable.ScrollContainer minWidth={0} maxHeight={680}>
              <MantineTable
                stickyHeader
                highlightOnHover
                withColumnBorders
                withRowBorders
                withTableBorder
                style={{
                  display: 'grid',
                }}
              >
                <MantineTable.Thead
                  style={{
                    display: 'grid',
                    position: 'sticky',
                    top: 0,
                    zIndex: 3,
                  }}
                >
                  {table
                    .getHeaderGroups()
                    .map((headerGroup: AppHeaderGroup) => (
                      <MantineTable.Tr
                        key={headerGroup.id}
                        style={{ display: 'flex', width: '100%' }}
                      >
                        {headerGroup.headers
                          .filter((header: AppHeader) =>
                            header.column.getIsVisible(),
                          )
                          .map((header: AppHeader) => (
                            <table.AppHeader header={header} key={header.id}>
                              {(h) => (
                                <MantineTable.Th
                                  colSpan={h.colSpan}
                                  style={{
                                    display: 'flex',
                                    flexShrink: 0,
                                    alignItems: 'center',
                                    overflow: 'clip',
                                    whiteSpace: 'nowrap',
                                    textOverflow: 'ellipsis',
                                    flexGrow: h.getSize(),
                                    width: h.getSize(),
                                    padding: 8,
                                    position: 'relative',
                                    ...getCommonPinningStyles(h.column),
                                  }}
                                >
                                  <Box
                                    style={{
                                      position: 'relative',
                                      flex: 1,
                                      minWidth: 0,
                                      paddingRight: h.column.getCanResize()
                                        ? 8
                                        : 0,
                                    }}
                                  >
                                    {h.isPlaceholder ? null : <h.FlexRender />}
                                  </Box>
                                  <h.ResizeHandle />
                                </MantineTable.Th>
                              )}
                            </table.AppHeader>
                          ))}
                      </MantineTable.Tr>
                    ))}
                </MantineTable.Thead>
                <MantineTable.Tbody style={{ display: 'grid' }}>
                  {table.getRowModel().rows.map((row: AppRow) => (
                    <MantineTable.Tr
                      key={row.id}
                      data-selected={row.getIsSelected() || undefined}
                      aria-selected={row.getIsSelected()}
                      style={{
                        display: 'flex',
                        width: '100%',
                        background: row.getIsSelected()
                          ? 'var(--mantine-color-blue-light)'
                          : undefined,
                      }}
                    >
                      {row.getVisibleCells().map((cell: AppCell) => (
                        <table.AppCell cell={cell} key={cell.id}>
                          {(c) => (
                            <MantineTable.Td
                              style={{
                                display: 'flex',
                                flexShrink: 0,
                                alignItems: 'center',
                                overflow: 'hidden',
                                flexGrow: c.column.getSize(),
                                width: c.column.getSize(),
                                ...getCommonPinningStyles(
                                  c.column,
                                  row.getIsSelected(),
                                ),
                              }}
                            >
                              {c.getIsGrouped() ? (
                                <c.GroupedCell />
                              ) : (
                                <c.FlexRender />
                              )}
                            </MantineTable.Td>
                          )}
                        </table.AppCell>
                      ))}
                    </MantineTable.Tr>
                  ))}
                </MantineTable.Tbody>
              </MantineTable>
            </MantineTable.ScrollContainer>
            <table.Pagination />
          </Paper>
        </Stack>
      </Container>
    </table.AppTable>
  )
}

const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')

ReactDOM.createRoot(rootElement).render(
  <React.StrictMode>
    <MantineProvider defaultColorScheme="auto">
      <App />
      <TanStackDevtools plugins={[tableDevtoolsPlugin()]} />
    </MantineProvider>
  </React.StrictMode>,
)
'use client'

import * as React from 'react'
import { TanStackDevtools } from '@tanstack/react-devtools'
import * as ReactDOM from 'react-dom/client'
import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer'
import {
  ActionIcon,
  Box,
  Button,
  Container,
  Group,
  MantineProvider,
  Table as MantineTable,
  Menu,
  Paper,
  Stack,
  TextInput,
  Tooltip,
  useComputedColorScheme,
  useMantineColorScheme,
} from '@mantine/core'
import '@mantine/core/styles.css'
import {
  IconDeviceDesktop,
  IconMoon,
  IconSearch,
  IconSun,
} from '@tabler/icons-react'
import {
  tableDevtoolsPlugin,
  useTanStackTableDevtools,
} from '@tanstack/react-table-devtools'
import type {
  Cell,
  Column,
  Header,
  HeaderGroup,
  Row,
} from '@tanstack/react-table'
import type { ExtendedColumnFilter } from '@/types'
import type { Person } from '@/lib/make-data'
import type { features } from '@/hooks/features'
import { makeData } from '@/lib/make-data'
import { useAppTable } from '@/hooks/table'
import { columns } from '@/columns'
import './styles/globals.css'

type AppColumn = Column<typeof features, Person, any>
type AppHeader = Header<typeof features, Person, any>
type AppRow = Row<typeof features, Person>
type AppCell = Cell<typeof features, Person, any>
type AppHeaderGroup = HeaderGroup<typeof features, Person>

function getCommonPinningStyles(
  column: AppColumn,
  isSelected = false,
): React.CSSProperties {
  const isPinned = column.getIsPinned()
  const isLastLeftPinnedColumn =
    isPinned === 'start' && column.getIsLastColumn('start')
  const isFirstRightPinnedColumn =
    isPinned === 'end' && column.getIsFirstColumn('end')

  return {
    boxShadow: isLastLeftPinnedColumn
      ? '-4px 0 4px -4px var(--mantine-color-default-border) inset'
      : isFirstRightPinnedColumn
        ? '4px 0 4px -4px var(--mantine-color-default-border) inset'
        : undefined,
    insetInlineStart:
      isPinned === 'start' ? `${column.getStart('start')}px` : undefined,
    insetInlineEnd:
      isPinned === 'end' ? `${column.getAfter('end')}px` : undefined,
    position: isPinned ? 'sticky' : 'relative',
    borderRight: isLastLeftPinnedColumn
      ? '1px solid var(--mantine-color-default-border)'
      : undefined,
    borderLeft: isFirstRightPinnedColumn
      ? '1px solid var(--mantine-color-default-border)'
      : undefined,
    background: isSelected
      ? 'var(--mantine-color-blue-light)'
      : isPinned
        ? 'var(--mantine-color-body)'
        : undefined,
    zIndex: isPinned ? 2 : 0,
  }
}

function ModeMenu() {
  const { colorScheme, setColorScheme } = useMantineColorScheme()
  const computedColorScheme = useComputedColorScheme('light')
  const icon =
    computedColorScheme === 'dark' ? (
      <IconMoon size={18} />
    ) : (
      <IconSun size={18} />
    )

  return (
    <Menu shadow="md" width={150}>
      <Menu.Target>
        <Tooltip label="Theme">
          <ActionIcon variant="subtle" aria-label="Theme">
            {icon}
          </ActionIcon>
        </Tooltip>
      </Menu.Target>
      <Menu.Dropdown>
        {(
          [
            { value: 'light', label: 'Light', icon: <IconSun size={16} /> },
            { value: 'dark', label: 'Dark', icon: <IconMoon size={16} /> },
            {
              value: 'auto',
              label: 'Auto',
              icon: <IconDeviceDesktop size={16} />,
            },
          ] satisfies Array<{
            value: 'light' | 'dark' | 'auto'
            label: string
            icon: React.ReactNode
          }>
        ).map((item) => (
          <Menu.Item
            key={item.value}
            leftSection={item.icon}
            color={colorScheme === item.value ? 'blue' : undefined}
            onClick={() => setColorScheme(item.value)}
          >
            {item.label}
          </Menu.Item>
        ))}
      </Menu.Dropdown>
    </Menu>
  )
}

function DebouncedTextInput({
  value: initialValue,
  onChange,
  debounce = 300,
  ...props
}: {
  value: string | number
  onChange: (value: string | number) => void
  debounce?: number
} & Omit<React.ComponentProps<typeof TextInput>, 'onChange'>) {
  const [value, setValue] = React.useState(initialValue)

  React.useEffect(() => {
    setValue(initialValue)
  }, [initialValue])

  const debouncedOnChange = useDebouncedCallback(onChange, { wait: debounce })

  return (
    <TextInput
      {...props}
      value={value}
      onChange={(event) => {
        setValue(event.currentTarget.value)
        debouncedOnChange(event.currentTarget.value)
      }}
    />
  )
}

function App() {
  const [columnFilters, setColumnFilters] = React.useState<
    Array<ExtendedColumnFilter>
  >([])
  const [globalFilter, setGlobalFilter] = React.useState('')
  const [data, setData] = React.useState(() => makeData(1_000))

  const refreshData = () => setData(makeData(1_000))
  const stressTest = () => setData(makeData(1_000_000))

  const table = useAppTable(
    {
      key: 'kitchen-sink-mantine',
      columns,
      data,
      debugTable: true,
      state: {
        columnFilters,
        globalFilter,
      },
      onColumnFiltersChange: setColumnFilters,
      onGlobalFilterChange: setGlobalFilter,
      initialState: {
        columnPinning: { start: ['select'], end: ['actions'] },
        columnOrder: columns.map((c) => c.id ?? ''),
      },
    },
    (state) => state, // default selector
  )

  useTanStackTableDevtools(table)

  return (
    <table.AppTable>
      <Container fluid py="md">
        <Stack gap="md">
          <Paper withBorder p="sm">
            <Group justify="flex-end" gap="xs">
              <ModeMenu />
              <Button variant="outline" size="sm" onClick={refreshData}>
                Regenerate Data
              </Button>
              <Button variant="outline" size="sm" onClick={stressTest}>
                Stress Test (1M rows)
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() =>
                  console.info(
                    'table.getSelectedRowModel().flatRows',
                    table.getSelectedRowModel().flatRows,
                  )
                }
              >
                Log Selected Rows
              </Button>
            </Group>
          </Paper>

          <Group align="center" gap="xs">
            <DebouncedTextInput
              value={globalFilter}
              onChange={(value) => setGlobalFilter(String(value))}
              placeholder="Search all columns..."
              leftSection={<IconSearch size={16} />}
              w={{ base: '100%', md: 360 }}
            />
            <table.FilterList />
            <table.SortList />
            <table.ViewOptions />
          </Group>

          <Paper withBorder>
            <MantineTable.ScrollContainer minWidth={0} maxHeight={680}>
              <MantineTable
                stickyHeader
                highlightOnHover
                withColumnBorders
                withRowBorders
                withTableBorder
                style={{
                  display: 'grid',
                }}
              >
                <MantineTable.Thead
                  style={{
                    display: 'grid',
                    position: 'sticky',
                    top: 0,
                    zIndex: 3,
                  }}
                >
                  {table
                    .getHeaderGroups()
                    .map((headerGroup: AppHeaderGroup) => (
                      <MantineTable.Tr
                        key={headerGroup.id}
                        style={{ display: 'flex', width: '100%' }}
                      >
                        {headerGroup.headers
                          .filter((header: AppHeader) =>
                            header.column.getIsVisible(),
                          )
                          .map((header: AppHeader) => (
                            <table.AppHeader header={header} key={header.id}>
                              {(h) => (
                                <MantineTable.Th
                                  colSpan={h.colSpan}
                                  style={{
                                    display: 'flex',
                                    flexShrink: 0,
                                    alignItems: 'center',
                                    overflow: 'clip',
                                    whiteSpace: 'nowrap',
                                    textOverflow: 'ellipsis',
                                    flexGrow: h.getSize(),
                                    width: h.getSize(),
                                    padding: 8,
                                    position: 'relative',
                                    ...getCommonPinningStyles(h.column),
                                  }}
                                >
                                  <Box
                                    style={{
                                      position: 'relative',
                                      flex: 1,
                                      minWidth: 0,
                                      paddingRight: h.column.getCanResize()
                                        ? 8
                                        : 0,
                                    }}
                                  >
                                    {h.isPlaceholder ? null : <h.FlexRender />}
                                  </Box>
                                  <h.ResizeHandle />
                                </MantineTable.Th>
                              )}
                            </table.AppHeader>
                          ))}
                      </MantineTable.Tr>
                    ))}
                </MantineTable.Thead>
                <MantineTable.Tbody style={{ display: 'grid' }}>
                  {table.getRowModel().rows.map((row: AppRow) => (
                    <MantineTable.Tr
                      key={row.id}
                      data-selected={row.getIsSelected() || undefined}
                      aria-selected={row.getIsSelected()}
                      style={{
                        display: 'flex',
                        width: '100%',
                        background: row.getIsSelected()
                          ? 'var(--mantine-color-blue-light)'
                          : undefined,
                      }}
                    >
                      {row.getVisibleCells().map((cell: AppCell) => (
                        <table.AppCell cell={cell} key={cell.id}>
                          {(c) => (
                            <MantineTable.Td
                              style={{
                                display: 'flex',
                                flexShrink: 0,
                                alignItems: 'center',
                                overflow: 'hidden',
                                flexGrow: c.column.getSize(),
                                width: c.column.getSize(),
                                ...getCommonPinningStyles(
                                  c.column,
                                  row.getIsSelected(),
                                ),
                              }}
                            >
                              {c.getIsGrouped() ? (
                                <c.GroupedCell />
                              ) : (
                                <c.FlexRender />
                              )}
                            </MantineTable.Td>
                          )}
                        </table.AppCell>
                      ))}
                    </MantineTable.Tr>
                  ))}
                </MantineTable.Tbody>
              </MantineTable>
            </MantineTable.ScrollContainer>
            <table.Pagination />
          </Paper>
        </Stack>
      </Container>
    </table.AppTable>
  )
}

const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')

ReactDOM.createRoot(rootElement).render(
  <React.StrictMode>
    <MantineProvider defaultColorScheme="auto">
      <App />
      <TanStackDevtools plugins={[tableDevtoolsPlugin()]} />
    </MantineProvider>
  </React.StrictMode>,
)