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 Chakra Ui

'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 {
  Box,
  Button,
  ChakraProvider,
  Container,
  HStack,
  IconButton,
  Stack,
  Table,
  defaultSystem,
} from '@chakra-ui/react'
import { ThemeProvider, useTheme } from 'next-themes'
import {
  IconDeviceDesktop,
  IconMoon,
  IconSearch,
  IconSun,
} from '@tabler/icons-react'
import {
  tableDevtoolsPlugin,
  useTanStackTableDevtools,
} from '@tanstack/react-table-devtools'
import type { Column } 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 {
  DropdownMenu,
  DropdownMenuItem,
  TextInput,
} from '@/components/data-table/shared'
import './styles/globals.css'

// ---------------------------------------------------------------------------
// Pinning styles (local to main - uses Chakra CSS vars)
// ---------------------------------------------------------------------------

function getCommonPinningStyles(
  column: Column<typeof features, Person>,
  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(--chakra-colors-border) inset'
      : isFirstRightPinnedColumn
        ? '4px 0 4px -4px var(--chakra-colors-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(--chakra-colors-border)'
      : undefined,
    borderLeft: isFirstRightPinnedColumn
      ? '1px solid var(--chakra-colors-border)'
      : undefined,
    background: isSelected
      ? 'var(--chakra-colors-blue-subtle)'
      : isPinned
        ? 'var(--chakra-colors-bg)'
        : undefined,
    zIndex: isPinned ? 2 : 0,
  }
}

// ---------------------------------------------------------------------------
// Debounced Text Input
// ---------------------------------------------------------------------------

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)
      }}
    />
  )
}

// ---------------------------------------------------------------------------
// Mode Menu
// ---------------------------------------------------------------------------

function ModeMenu() {
  const { resolvedTheme, setTheme, theme } = useTheme()
  const icon =
    resolvedTheme === 'dark' ? <IconMoon size={18} /> : <IconSun size={18} />

  return (
    <DropdownMenu
      width="150px"
      trigger={
        <IconButton variant="subtle" aria-label="Theme" title="Theme">
          {icon}
        </IconButton>
      }
    >
      {(
        [
          { value: 'light', label: 'Light', icon: <IconSun size={16} /> },
          { value: 'dark', label: 'Dark', icon: <IconMoon size={16} /> },
          {
            value: 'system',
            label: 'System',
            icon: <IconDeviceDesktop size={16} />,
          },
        ] satisfies Array<{
          value: 'light' | 'dark' | 'system'
          label: string
          icon: React.ReactNode
        }>
      ).map((item) => (
        <DropdownMenuItem
          key={item.value}
          value={`theme-${item.value}`}
          icon={item.icon}
          colorPalette={theme === item.value ? 'blue' : undefined}
          onSelect={() => setTheme(item.value)}
        >
          {item.label}
        </DropdownMenuItem>
      ))}
    </DropdownMenu>
  )
}

// ---------------------------------------------------------------------------
// Chakra Example Provider
// ---------------------------------------------------------------------------

function ChakraExampleProvider({ children }: { children: React.ReactNode }) {
  return (
    <ChakraProvider value={defaultSystem}>
      <ThemeProvider
        attribute="class"
        defaultTheme="system"
        enableSystem
        disableTransitionOnChange
        // This client-only example applies themes through effects, so the SSR
        // initialization script should remain inert.
        scriptProps={{ type: 'text/plain' }}
      >
        {children}
      </ThemeProvider>
    </ChakraProvider>
  )
}

// ---------------------------------------------------------------------------
// App
// ---------------------------------------------------------------------------

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-chakra-ui',
      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 maxW="none" py="4">
        <Stack gap="4">
          <Box borderWidth="1px" rounded="md" p="3">
            <HStack justify="flex-end" gap="2">
              <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>
            </HStack>
          </Box>

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

          <Box borderWidth="1px" rounded="md">
            <Box overflow="auto" maxH="680px">
              <Table.Root
                stickyHeader
                interactive
                showColumnBorder
                variant="outline"
                style={{
                  tableLayout: 'fixed',
                }}
              >
                <Table.Header>
                  {table.getHeaderGroups().map((headerGroup) => (
                    // Chakra's `stickyHeader` recipe pins this row with
                    // zIndex 1; lift it above the body's pinned cells
                    // (zIndex 2) so rows don't paint over the header.
                    <Table.Row key={headerGroup.id} style={{ zIndex: 3 }}>
                      {headerGroup.headers
                        .filter((header) => header.column.getIsVisible())
                        .map((header) => (
                          <table.AppHeader header={header} key={header.id}>
                            {(h) => (
                              <Table.ColumnHeader
                                colSpan={h.colSpan}
                                align={
                                  h.column.id === 'select'
                                    ? 'center'
                                    : undefined
                                }
                                style={{
                                  width: h.getSize(),
                                  padding: 8,
                                  position: 'relative',
                                  // Clip keeps the header content (title + menu
                                  // chevron) inside the cell so it can't spill
                                  // over the resize handle. Kept as a normal
                                  // table cell (no display override) so the
                                  // fixed table layout still works.
                                  overflow: 'hidden',
                                  ...getCommonPinningStyles(h.column),
                                  // Opaque in every header cell (not just
                                  // pinned ones) so body content doesn't
                                  // show through the sticky header.
                                  background: 'var(--chakra-colors-bg)',
                                }}
                              >
                                <Box
                                  style={{
                                    position: 'relative',
                                    overflow: 'hidden',
                                    // Leave room for the resize handle.
                                    paddingRight: h.column.getCanResize()
                                      ? 8
                                      : 0,
                                  }}
                                >
                                  {h.isPlaceholder ? null : <h.FlexRender />}
                                </Box>
                                <h.ResizeHandle />
                              </Table.ColumnHeader>
                            )}
                          </table.AppHeader>
                        ))}
                    </Table.Row>
                  ))}
                </Table.Header>
                <Table.Body>
                  {table.getRowModel().rows.map((row) => {
                    const selected = row.getIsSelected()
                    return (
                      <Table.Row
                        key={row.id}
                        aria-selected={selected}
                        data-selected={selected || undefined}
                        bg={
                          selected
                            ? 'var(--chakra-colors-blue-subtle)'
                            : undefined
                        }
                      >
                        {row.getVisibleCells().map((cell) => (
                          <table.AppCell cell={cell} key={cell.id}>
                            {(c) => (
                              <Table.Cell
                                align={
                                  c.column.id === 'select'
                                    ? 'center'
                                    : undefined
                                }
                                style={{
                                  width: c.column.getSize(),
                                  overflow: 'hidden',
                                  ...getCommonPinningStyles(c.column, selected),
                                }}
                              >
                                {c.getIsGrouped() ? (
                                  <c.GroupedCell />
                                ) : (
                                  <c.FlexRender />
                                )}
                              </Table.Cell>
                            )}
                          </table.AppCell>
                        ))}
                      </Table.Row>
                    )
                  })}
                </Table.Body>
              </Table.Root>
            </Box>
            <table.Pagination />
          </Box>
        </Stack>
      </Container>
    </table.AppTable>
  )
}

// ---------------------------------------------------------------------------
// Root
// ---------------------------------------------------------------------------

function Root() {
  return (
    <ChakraExampleProvider>
      <App />
      <TanStackDevtools plugins={[tableDevtoolsPlugin()]} />
    </ChakraExampleProvider>
  )
}

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

ReactDOM.createRoot(rootElement).render(
  <React.StrictMode>
    <Root />
  </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 {
  Box,
  Button,
  ChakraProvider,
  Container,
  HStack,
  IconButton,
  Stack,
  Table,
  defaultSystem,
} from '@chakra-ui/react'
import { ThemeProvider, useTheme } from 'next-themes'
import {
  IconDeviceDesktop,
  IconMoon,
  IconSearch,
  IconSun,
} from '@tabler/icons-react'
import {
  tableDevtoolsPlugin,
  useTanStackTableDevtools,
} from '@tanstack/react-table-devtools'
import type { Column } 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 {
  DropdownMenu,
  DropdownMenuItem,
  TextInput,
} from '@/components/data-table/shared'
import './styles/globals.css'

// ---------------------------------------------------------------------------
// Pinning styles (local to main - uses Chakra CSS vars)
// ---------------------------------------------------------------------------

function getCommonPinningStyles(
  column: Column<typeof features, Person>,
  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(--chakra-colors-border) inset'
      : isFirstRightPinnedColumn
        ? '4px 0 4px -4px var(--chakra-colors-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(--chakra-colors-border)'
      : undefined,
    borderLeft: isFirstRightPinnedColumn
      ? '1px solid var(--chakra-colors-border)'
      : undefined,
    background: isSelected
      ? 'var(--chakra-colors-blue-subtle)'
      : isPinned
        ? 'var(--chakra-colors-bg)'
        : undefined,
    zIndex: isPinned ? 2 : 0,
  }
}

// ---------------------------------------------------------------------------
// Debounced Text Input
// ---------------------------------------------------------------------------

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)
      }}
    />
  )
}

// ---------------------------------------------------------------------------
// Mode Menu
// ---------------------------------------------------------------------------

function ModeMenu() {
  const { resolvedTheme, setTheme, theme } = useTheme()
  const icon =
    resolvedTheme === 'dark' ? <IconMoon size={18} /> : <IconSun size={18} />

  return (
    <DropdownMenu
      width="150px"
      trigger={
        <IconButton variant="subtle" aria-label="Theme" title="Theme">
          {icon}
        </IconButton>
      }
    >
      {(
        [
          { value: 'light', label: 'Light', icon: <IconSun size={16} /> },
          { value: 'dark', label: 'Dark', icon: <IconMoon size={16} /> },
          {
            value: 'system',
            label: 'System',
            icon: <IconDeviceDesktop size={16} />,
          },
        ] satisfies Array<{
          value: 'light' | 'dark' | 'system'
          label: string
          icon: React.ReactNode
        }>
      ).map((item) => (
        <DropdownMenuItem
          key={item.value}
          value={`theme-${item.value}`}
          icon={item.icon}
          colorPalette={theme === item.value ? 'blue' : undefined}
          onSelect={() => setTheme(item.value)}
        >
          {item.label}
        </DropdownMenuItem>
      ))}
    </DropdownMenu>
  )
}

// ---------------------------------------------------------------------------
// Chakra Example Provider
// ---------------------------------------------------------------------------

function ChakraExampleProvider({ children }: { children: React.ReactNode }) {
  return (
    <ChakraProvider value={defaultSystem}>
      <ThemeProvider
        attribute="class"
        defaultTheme="system"
        enableSystem
        disableTransitionOnChange
        // This client-only example applies themes through effects, so the SSR
        // initialization script should remain inert.
        scriptProps={{ type: 'text/plain' }}
      >
        {children}
      </ThemeProvider>
    </ChakraProvider>
  )
}

// ---------------------------------------------------------------------------
// App
// ---------------------------------------------------------------------------

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-chakra-ui',
      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 maxW="none" py="4">
        <Stack gap="4">
          <Box borderWidth="1px" rounded="md" p="3">
            <HStack justify="flex-end" gap="2">
              <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>
            </HStack>
          </Box>

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

          <Box borderWidth="1px" rounded="md">
            <Box overflow="auto" maxH="680px">
              <Table.Root
                stickyHeader
                interactive
                showColumnBorder
                variant="outline"
                style={{
                  tableLayout: 'fixed',
                }}
              >
                <Table.Header>
                  {table.getHeaderGroups().map((headerGroup) => (
                    // Chakra's `stickyHeader` recipe pins this row with
                    // zIndex 1; lift it above the body's pinned cells
                    // (zIndex 2) so rows don't paint over the header.
                    <Table.Row key={headerGroup.id} style={{ zIndex: 3 }}>
                      {headerGroup.headers
                        .filter((header) => header.column.getIsVisible())
                        .map((header) => (
                          <table.AppHeader header={header} key={header.id}>
                            {(h) => (
                              <Table.ColumnHeader
                                colSpan={h.colSpan}
                                align={
                                  h.column.id === 'select'
                                    ? 'center'
                                    : undefined
                                }
                                style={{
                                  width: h.getSize(),
                                  padding: 8,
                                  position: 'relative',
                                  // Clip keeps the header content (title + menu
                                  // chevron) inside the cell so it can't spill
                                  // over the resize handle. Kept as a normal
                                  // table cell (no display override) so the
                                  // fixed table layout still works.
                                  overflow: 'hidden',
                                  ...getCommonPinningStyles(h.column),
                                  // Opaque in every header cell (not just
                                  // pinned ones) so body content doesn't
                                  // show through the sticky header.
                                  background: 'var(--chakra-colors-bg)',
                                }}
                              >
                                <Box
                                  style={{
                                    position: 'relative',
                                    overflow: 'hidden',
                                    // Leave room for the resize handle.
                                    paddingRight: h.column.getCanResize()
                                      ? 8
                                      : 0,
                                  }}
                                >
                                  {h.isPlaceholder ? null : <h.FlexRender />}
                                </Box>
                                <h.ResizeHandle />
                              </Table.ColumnHeader>
                            )}
                          </table.AppHeader>
                        ))}
                    </Table.Row>
                  ))}
                </Table.Header>
                <Table.Body>
                  {table.getRowModel().rows.map((row) => {
                    const selected = row.getIsSelected()
                    return (
                      <Table.Row
                        key={row.id}
                        aria-selected={selected}
                        data-selected={selected || undefined}
                        bg={
                          selected
                            ? 'var(--chakra-colors-blue-subtle)'
                            : undefined
                        }
                      >
                        {row.getVisibleCells().map((cell) => (
                          <table.AppCell cell={cell} key={cell.id}>
                            {(c) => (
                              <Table.Cell
                                align={
                                  c.column.id === 'select'
                                    ? 'center'
                                    : undefined
                                }
                                style={{
                                  width: c.column.getSize(),
                                  overflow: 'hidden',
                                  ...getCommonPinningStyles(c.column, selected),
                                }}
                              >
                                {c.getIsGrouped() ? (
                                  <c.GroupedCell />
                                ) : (
                                  <c.FlexRender />
                                )}
                              </Table.Cell>
                            )}
                          </table.AppCell>
                        ))}
                      </Table.Row>
                    )
                  })}
                </Table.Body>
              </Table.Root>
            </Box>
            <table.Pagination />
          </Box>
        </Stack>
      </Container>
    </table.AppTable>
  )
}

// ---------------------------------------------------------------------------
// Root
// ---------------------------------------------------------------------------

function Root() {
  return (
    <ChakraExampleProvider>
      <App />
      <TanStackDevtools plugins={[tableDevtoolsPlugin()]} />
    </ChakraExampleProvider>
  )
}

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

ReactDOM.createRoot(rootElement).render(
  <React.StrictMode>
    <Root />
  </React.StrictMode>,
)