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

Solid Example: Kitchen Sink

import { faker } from '@faker-js/faker'
import {
  aggregationFns,
  createExpandedRowModel,
  createFacetedMinMaxValues,
  createFacetedRowModel,
  createFacetedUniqueValues,
  createFilteredRowModel,
  createGroupedRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  createTableHook,
  filterFns,
  sortFns,
  stockFeatures,
} from '@tanstack/solid-table'
import { useTanStackTableDevtools } from '@tanstack/solid-table-devtools'
import { compareItems, rankItem } from '@tanstack/match-sorter-utils'
import {
  For,
  createEffect,
  createMemo,
  createSignal,
  onCleanup,
} from 'solid-js'
import { makeData } from './makeData'
import type { JSX } from 'solid-js'
import type { RankingInfo } from '@tanstack/match-sorter-utils'
import type { Person } from './makeData'
import type {
  Cell,
  CellData,
  Column,
  FilterFn,
  Header,
  Row,
  RowData,
  SortFn,
  TableFeatures,
} from '@tanstack/solid-table'

declare module '@tanstack/solid-table' {
  interface ColumnMeta<
    TFeatures extends TableFeatures,
    TData extends RowData,
    TValue extends CellData = CellData,
  > {
    filterVariant?: 'text' | 'range' | 'select'
  }
  interface FilterFns {
    fuzzy: FilterFn<typeof stockFeatures, Person>
  }
  interface FilterMeta {
    itemRank?: RankingInfo
  }
}

const { createAppTable, createAppColumnHelper } = createTableHook({
  features: stockFeatures,
  rowModels: {
    expandedRowModel: createExpandedRowModel(),
    filteredRowModel: createFilteredRowModel({
      ...filterFns,
      fuzzy: (row, columnId, value, addMeta) => {
        const itemRank = rankItem(row.getValue(columnId), value)
        addMeta?.({ itemRank })
        return itemRank.passed
      },
    }),
    facetedRowModel: createFacetedRowModel(),
    facetedMinMaxValues: createFacetedMinMaxValues(),
    facetedUniqueValues: createFacetedUniqueValues(),
    groupedRowModel: createGroupedRowModel(aggregationFns),
    paginatedRowModel: createPaginatedRowModel(),
    sortedRowModel: createSortedRowModel(sortFns),
  },
})

const fuzzySort: SortFn<typeof stockFeatures, Person> = (
  rowA,
  rowB,
  columnId,
) => {
  let dir = 0
  if (rowA.columnFiltersMeta[columnId]) {
    dir = compareItems(
      rowA.columnFiltersMeta[columnId].itemRank!,
      rowB.columnFiltersMeta[columnId].itemRank!,
    )
  }
  return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir
}

const sortStatusFn: SortFn<typeof stockFeatures, Person> = (rowA, rowB) => {
  const statusOrder = ['single', 'complicated', 'relationship']
  return (
    statusOrder.indexOf(rowA.original.status) -
    statusOrder.indexOf(rowB.original.status)
  )
}

const getCommonPinningStyles = (
  column: Column<typeof stockFeatures, Person>,
): JSX.CSSProperties => {
  const isPinned = column.getIsPinned()
  const isLastLeftPinnedColumn =
    isPinned === 'left' && column.getIsLastColumn('left')
  const isFirstRightPinnedColumn =
    isPinned === 'right' && column.getIsFirstColumn('right')

  return {
    'box-shadow': isLastLeftPinnedColumn
      ? '-4px 0 4px -4px gray inset'
      : isFirstRightPinnedColumn
        ? '4px 0 4px -4px gray inset'
        : undefined,
    left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined,
    right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined,
    opacity: isPinned ? 0.97 : 1,
    position: isPinned ? 'sticky' : 'relative',
    'z-index': isPinned ? 1 : 0,
  }
}

function IndeterminateCheckbox(
  props: {
    indeterminate?: boolean
  } & JSX.InputHTMLAttributes<HTMLInputElement>,
) {
  let ref!: HTMLInputElement
  createEffect(() => {
    if (typeof props.indeterminate === 'boolean') {
      ref.indeterminate = !props.checked && props.indeterminate
    }
  })

  return <input type="checkbox" ref={ref} {...props} />
}

function DebouncedInput(
  props: {
    value: string | number
    onChange: (value: string | number) => void
    debounce?: number
  } & Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'>,
) {
  const [value, setValue] = createSignal(props.value)

  createEffect(() => {
    setValue(props.value)
  })

  createEffect(() => {
    const currentValue = value()
    const timeout = setTimeout(
      () => props.onChange(currentValue),
      props.debounce ?? 300,
    )
    onCleanup(() => clearTimeout(timeout))
  })

  return (
    <input
      {...props}
      value={value()}
      onInput={(e) => setValue(e.currentTarget.value)}
    />
  )
}

function Filter(props: { column: Column<typeof stockFeatures, Person> }) {
  const filterVariant = () => props.column.columnDef.meta?.filterVariant
  const sortedUniqueValues = createMemo(() =>
    filterVariant() === 'range'
      ? []
      : Array.from(props.column.getFacetedUniqueValues().keys())
          .sort()
          .slice(0, 5000),
  )

  return (
    <>
      {filterVariant() === 'range' ? (
        <div class="filter-row">
          <DebouncedInput
            type="number"
            min={Number(props.column.getFacetedMinMaxValues()?.[0] ?? '')}
            max={Number(props.column.getFacetedMinMaxValues()?.[1] ?? '')}
            value={
              (
                props.column.getFilterValue() as [number, number] | undefined
              )?.[0] ?? ''
            }
            onChange={(value) =>
              props.column.setFilterValue(
                (old: [number, number] | undefined) => [value, old?.[1]],
              )
            }
            placeholder={`Min${
              props.column.getFacetedMinMaxValues()?.[0] !== undefined
                ? ` (${props.column.getFacetedMinMaxValues()?.[0]})`
                : ''
            }`}
            class="filter-input"
          />
          <DebouncedInput
            type="number"
            min={Number(props.column.getFacetedMinMaxValues()?.[0] ?? '')}
            max={Number(props.column.getFacetedMinMaxValues()?.[1] ?? '')}
            value={
              (
                props.column.getFilterValue() as [number, number] | undefined
              )?.[1] ?? ''
            }
            onChange={(value) =>
              props.column.setFilterValue(
                (old: [number, number] | undefined) => [old?.[0], value],
              )
            }
            placeholder={`Max${
              props.column.getFacetedMinMaxValues()?.[1] !== undefined
                ? ` (${props.column.getFacetedMinMaxValues()?.[1]})`
                : ''
            }`}
            class="filter-input"
          />
        </div>
      ) : filterVariant() === 'select' ? (
        <select
          onChange={(e) => props.column.setFilterValue(e.currentTarget.value)}
          value={(props.column.getFilterValue() ?? '').toString()}
          class="filter-select"
        >
          <option value="">All</option>
          <For each={sortedUniqueValues()}>
            {(value) => <option value={String(value)}>{String(value)}</option>}
          </For>
        </select>
      ) : (
        <>
          <datalist id={props.column.id + 'list'}>
            <For each={sortedUniqueValues()}>
              {(value) => <option value={String(value)} />}
            </For>
          </datalist>
          <DebouncedInput
            type="text"
            value={(props.column.getFilterValue() ?? '') as string}
            onChange={(value) => props.column.setFilterValue(value)}
            placeholder={`Search (${props.column.getFacetedUniqueValues().size})`}
            class="filter-select"
            list={props.column.id + 'list'}
          />
        </>
      )}
    </>
  )
}

type AppTable = ReturnType<typeof createAppTable<Person>>

function TableHeader(props: {
  header: Header<typeof stockFeatures, Person, unknown>
  table: AppTable
}) {
  const column = () => props.header.column
  const style = () => ({
    ...getCommonPinningStyles(column()),
    'white-space': 'nowrap',
    width: `calc(var(--header-${props.header.id}-size) * 1px)`,
  })

  return (
    <th style={style()} colSpan={props.header.colSpan}>
      {!props.header.isPlaceholder ? (
        <>
          <div class="header-row">
            <div style={{ flex: 1, 'min-width': 0 }}>
              <div class="header-controls">
                {column().getCanPin() ? (
                  <span class="pin-actions">
                    {column().getIsPinned() !== 'left' ? (
                      <button
                        class="pin-button"
                        onClick={() => column().pin('left')}
                      >
                        {'<'}
                      </button>
                    ) : null}
                    {column().getIsPinned() ? (
                      <button
                        class="pin-button"
                        onClick={() => column().pin(false)}
                      >
                        x
                      </button>
                    ) : null}
                    {column().getIsPinned() !== 'right' ? (
                      <button
                        class="pin-button"
                        onClick={() => column().pin('right')}
                      >
                        {'>'}
                      </button>
                    ) : null}
                  </span>
                ) : null}
                {column().getCanGroup() ? (
                  <button
                    class="pin-button"
                    onClick={column().getToggleGroupingHandler()}
                  >
                    {column().getIsGrouped()
                      ? `Stop (${column().getGroupedIndex()})`
                      : 'Group'}
                  </button>
                ) : null}
              </div>
              {column().getCanSort() ? (
                <span
                  class="sortable-header"
                  onClick={column().getToggleSortingHandler()}
                >
                  <props.table.FlexRender header={props.header} />
                  {{
                    asc: ' â–²',
                    desc: ' â–¼',
                  }[column().getIsSorted() as string] ?? null}
                </span>
              ) : (
                <props.table.FlexRender header={props.header} />
              )}
              {column().getCanFilter() ? (
                <div>
                  <Filter column={column()} />
                </div>
              ) : null}
            </div>
          </div>
          {column().getCanResize() ? (
            <div
              onDblClick={() => column().resetSize()}
              onMouseDown={props.header.getResizeHandler()}
              onTouchStart={props.header.getResizeHandler()}
              class={`resizer ${column().getIsResizing() ? 'isResizing' : ''}`}
            />
          ) : null}
        </>
      ) : null}
    </th>
  )
}

function TableCell(props: {
  cell: Cell<typeof stockFeatures, Person, unknown>
  table: AppTable
}) {
  const className = () => {
    const groupingActive = props.table.atoms.grouping.get().length > 0
    const hasAggregation = !!props.cell.column.columnDef.aggregationFn
    return !groupingActive
      ? undefined
      : props.cell.getIsGrouped()
        ? 'cell-grouped'
        : hasAggregation && props.cell.getIsAggregated()
          ? 'cell-aggregated'
          : props.cell.getIsPlaceholder()
            ? 'cell-placeholder'
            : undefined
  }

  return (
    <td
      style={{
        ...getCommonPinningStyles(props.cell.column),
        width: `calc(var(--col-${props.cell.column.id}-size) * 1px)`,
      }}
      class={className()}
    >
      {props.cell.getIsGrouped() ? (
        <button
          onClick={props.cell.row.getToggleExpandedHandler()}
          style={{
            cursor: props.cell.row.getCanExpand() ? 'pointer' : 'normal',
          }}
        >
          {props.cell.row.getIsExpanded() ? 'v' : '>'}{' '}
          <props.table.FlexRender cell={props.cell} /> (
          {props.cell.row.subRows.length.toLocaleString()})
        </button>
      ) : (
        <props.table.FlexRender cell={props.cell} />
      )}
    </td>
  )
}

function PinnedRow(props: {
  row: Row<typeof stockFeatures, Person>
  table: AppTable
}) {
  const bottomRows = () => props.table.getBottomRows()
  return (
    <tr
      class="pinned-row"
      style={{
        position: 'sticky',
        top:
          props.row.getIsPinned() === 'top'
            ? `${props.row.getPinnedIndex() * 32 + 48}px`
            : undefined,
        bottom:
          props.row.getIsPinned() === 'bottom'
            ? `${(bottomRows().length - 1 - props.row.getPinnedIndex()) * 32}px`
            : undefined,
        'z-index': 1,
      }}
    >
      <For each={props.row.getVisibleCells()}>
        {(cell) => <TableCell cell={cell} table={props.table} />}
      </For>
    </tr>
  )
}

const columnHelper = createAppColumnHelper<Person>()

const columns = columnHelper.columns([
  columnHelper.display({
    id: 'select',
    size: 80,
    minSize: 80,
    maxSize: 80,
    enableSorting: false,
    enableGrouping: false,
    enableHiding: false,
    enableResizing: false,
    header: ({ table }) => (
      <IndeterminateCheckbox
        checked={table.getIsAllPageRowsSelected()}
        indeterminate={table.getIsSomePageRowsSelected()}
        onChange={table.getToggleAllPageRowsSelectedHandler()}
        title="Select all on this page"
      />
    ),
    cell: ({ row }) => (
      <div class="column-toggle-row">
        <IndeterminateCheckbox
          checked={row.getIsSelected()}
          disabled={!row.getCanSelect()}
          indeterminate={row.getIsSomeSelected()}
          onChange={row.getToggleSelectedHandler()}
        />{' '}
        <button
          class="pin-button"
          onClick={() => row.pin(row.getIsPinned() === 'top' ? false : 'top')}
        >
          {row.getIsPinned() === 'top' ? 'Pinned' : 'Pin'}
        </button>
      </div>
    ),
  }),
  columnHelper.accessor('firstName', {
    id: 'firstName',
    size: 200,
    header: 'First Name',
    filterFn: 'fuzzy',
    sortFn: fuzzySort,
    meta: { filterVariant: 'text' },
    getGroupingValue: (row) => `${row.firstName} ${row.lastName}`,
    cell: ({ row, getValue }) => (
      <div style={{ 'padding-left': `${row.depth * 1.5}rem` }}>
        {row.getCanExpand() ? (
          <button
            onClick={row.getToggleExpandedHandler()}
            style={{ cursor: 'pointer', 'margin-right': '0.25rem' }}
          >
            {row.getIsExpanded() ? 'v' : '>'}
          </button>
        ) : (
          <span style={{ 'margin-right': '0.25rem' }}>-</span>
        )}
        {String(getValue())}
      </div>
    ),
  }),
  columnHelper.accessor((row) => row.lastName, {
    id: 'lastName',
    size: 180,
    header: 'Last Name',
    meta: { filterVariant: 'text' },
  }),
  columnHelper.accessor('age', {
    id: 'age',
    size: 200,
    header: 'Age',
    meta: { filterVariant: 'range' },
    aggregationFn: 'median',
    aggregatedCell: ({ getValue }) =>
      Math.round(getValue<number>() * 100) / 100,
  }),
  columnHelper.accessor('visits', {
    id: 'visits',
    size: 200,
    header: 'Visits',
    meta: { filterVariant: 'range' },
    aggregationFn: 'sum',
    aggregatedCell: ({ getValue }) => getValue<number>().toLocaleString(),
  }),
  columnHelper.accessor('status', {
    id: 'status',
    size: 200,
    header: 'Status',
    sortFn: sortStatusFn,
    meta: { filterVariant: 'select' },
  }),
  columnHelper.accessor('progress', {
    id: 'progress',
    size: 200,
    header: 'Profile Progress',
    meta: { filterVariant: 'range' },
    aggregationFn: 'mean',
    cell: ({ getValue }) => `${Math.round(getValue<number>() * 100) / 100}%`,
    aggregatedCell: ({ getValue }) =>
      `${Math.round(getValue<number>() * 100) / 100}%`,
  }),
])

function App() {
  const [data, setData] = createSignal(makeData(1_000))
  const [, setRenderTick] = createSignal({})

  const table = createAppTable(
    {
      key: 'kitchen-sink', // needed for devtools
      columns,
      get data() {
        return data()
      },
      getSubRows: (row) => row.subRows,
      globalFilterFn: 'fuzzy',
      columnResizeMode: 'onChange',
      defaultColumn: { minSize: 200, maxSize: 800 },
      initialState: {
        columnOrder: columns.map((c) => c.id!),
        columnPinning: { left: ['select'], right: [] },
        pagination: { pageIndex: 0, pageSize: 20 },
      },
      keepPinnedRows: true,
      debugTable: true,
    },
    (state) => state,
  )

  useTanStackTableDevtools(table)

  const columnSizeVars = createMemo(() => {
    void table.atoms.columnResizing.get()
    void table.atoms.columnSizing.get()
    const colSizes: Record<string, number> = {}
    for (const header of table.getFlatHeaders()) {
      colSizes[`--header-${header.id}-size`] = header.getSize()
      colSizes[`--col-${header.column.id}-size`] = header.column.getSize()
    }
    return colSizes
  })

  const refreshData = () => setData(makeData(1_000))
  const nestedData = () => setData(makeData(100, 5, 3))
  const stress10k = () => setData(makeData(10_000))
  const stress100k = () => setData(makeData(100_000))
  const shuffleColumns = () => {
    table.setColumnOrder(
      faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)),
    )
  }

  return (
    <div class="demo-root">
      <h1>Kitchen Sink - All Features</h1>
      <div class="toolbar">
        <div class="toolbar-row">
          <DebouncedInput
            value={(table.atoms.globalFilter.get() ?? '') as string}
            onChange={(value) => table.setGlobalFilter(String(value))}
            class="global-filter-input"
            placeholder="Fuzzy search all columns..."
          />
        </div>
        <div class="toolbar-row">
          <button onClick={refreshData} class="demo-button demo-button-sm">
            Flat 1k
          </button>
          <button onClick={nestedData} class="demo-button demo-button-sm">
            Nested 100x5x3
          </button>
          <button onClick={stress10k} class="demo-button demo-button-sm">
            Stress 10k (flat)
          </button>
          <button onClick={stress100k} class="demo-button demo-button-sm">
            Stress 100k (flat)
          </button>
          <button
            onClick={() => table.reset()}
            class="demo-button demo-button-sm"
          >
            Reset Table
          </button>
          <button onClick={shuffleColumns} class="demo-button demo-button-sm">
            Shuffle Columns
          </button>
          <button
            onClick={() => setRenderTick({})}
            class="demo-button demo-button-sm"
          >
            Force Rerender
          </button>
          <span class="nowrap">
            {table.getSelectedRowModel().flatRows.length.toLocaleString()} of{' '}
            {table.getCoreRowModel().flatRows.length.toLocaleString()} selected
          </span>
        </div>
        <details class="column-toggle-panel">
          <summary class="column-toggle-panel-header">
            Column visibility
          </summary>
          <div class="column-toggle-row">
            <label>
              <input
                type="checkbox"
                checked={table.getIsAllColumnsVisible()}
                onChange={table.getToggleAllColumnsVisibilityHandler()}
              />{' '}
              Toggle All
            </label>
          </div>
          <For each={table.getAllLeafColumns()}>
            {(column) => (
              <div class="column-toggle-row">
                <label>
                  <input
                    type="checkbox"
                    checked={column.getIsVisible()}
                    disabled={!column.getCanHide()}
                    onChange={column.getToggleVisibilityHandler()}
                  />{' '}
                  {column.id}
                </label>
              </div>
            )}
          </For>
        </details>
      </div>
      <div class="table-container">
        <table
          style={{ ...columnSizeVars(), width: `${table.getTotalSize()}px` }}
        >
          <thead>
            <For each={table.getHeaderGroups()}>
              {(headerGroup) => (
                <tr>
                  <For each={headerGroup.headers}>
                    {(header) => <TableHeader header={header} table={table} />}
                  </For>
                </tr>
              )}
            </For>
          </thead>
          <tbody>
            <For each={table.getTopRows()}>
              {(row) => <PinnedRow row={row} table={table} />}
            </For>
            <For each={table.getCenterRows()}>
              {(row) => (
                <tr>
                  <For each={row.getVisibleCells()}>
                    {(cell) => <TableCell cell={cell} table={table} />}
                  </For>
                </tr>
              )}
            </For>
            <For each={table.getBottomRows()}>
              {(row) => <PinnedRow row={row} table={table} />}
            </For>
          </tbody>
        </table>
      </div>
      <div class="spacer-sm" />
      <div class="controls">
        <button
          class="demo-button demo-button-sm"
          onClick={() => table.setPageIndex(0)}
          disabled={!table.getCanPreviousPage()}
        >
          {'<<'}
        </button>
        <button
          class="demo-button demo-button-sm"
          onClick={() => table.previousPage()}
          disabled={!table.getCanPreviousPage()}
        >
          {'<'}
        </button>
        <button
          class="demo-button demo-button-sm"
          onClick={() => table.nextPage()}
          disabled={!table.getCanNextPage()}
        >
          {'>'}
        </button>
        <button
          class="demo-button demo-button-sm"
          onClick={() => table.setPageIndex(table.getPageCount() - 1)}
          disabled={!table.getCanNextPage()}
        >
          {'>>'}
        </button>
        <span class="inline-controls">
          <div>Page</div>
          <strong>
            {(table.atoms.pagination.get().pageIndex + 1).toLocaleString()} of{' '}
            {table.getPageCount().toLocaleString()}
          </strong>
        </span>
        <span class="inline-controls">
          | Go to page:
          <input
            type="number"
            min="1"
            max={table.getPageCount()}
            value={table.atoms.pagination.get().pageIndex + 1}
            onInput={(e) => {
              const page = e.currentTarget.value
                ? Number(e.currentTarget.value) - 1
                : 0
              table.setPageIndex(page)
            }}
            class="page-size-input"
          />
        </span>
        <select
          value={table.atoms.pagination.get().pageSize}
          onChange={(e) => table.setPageSize(Number(e.currentTarget.value))}
        >
          <For each={[10, 20, 30, 50, 100]}>
            {(pageSize) => <option value={pageSize}>Show {pageSize}</option>}
          </For>
        </select>
      </div>
      <div class="spacer-sm" />
      <div class="nowrap">
        {table.getRowModel().rows.length.toLocaleString()} rows on this page (
        {table.getFilteredRowModel().rows.length.toLocaleString()} filtered of{' '}
        {table.getCoreRowModel().rows.length.toLocaleString()} total)
      </div>
      <div class="spacer-md" />
      <details>
        <summary>Table state (live)</summary>
        <pre class="state-dump">
          {JSON.stringify(table.store.get(), null, 2)}
        </pre>
      </details>
    </div>
  )
}

export default App
import { faker } from '@faker-js/faker'
import {
  aggregationFns,
  createExpandedRowModel,
  createFacetedMinMaxValues,
  createFacetedRowModel,
  createFacetedUniqueValues,
  createFilteredRowModel,
  createGroupedRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  createTableHook,
  filterFns,
  sortFns,
  stockFeatures,
} from '@tanstack/solid-table'
import { useTanStackTableDevtools } from '@tanstack/solid-table-devtools'
import { compareItems, rankItem } from '@tanstack/match-sorter-utils'
import {
  For,
  createEffect,
  createMemo,
  createSignal,
  onCleanup,
} from 'solid-js'
import { makeData } from './makeData'
import type { JSX } from 'solid-js'
import type { RankingInfo } from '@tanstack/match-sorter-utils'
import type { Person } from './makeData'
import type {
  Cell,
  CellData,
  Column,
  FilterFn,
  Header,
  Row,
  RowData,
  SortFn,
  TableFeatures,
} from '@tanstack/solid-table'

declare module '@tanstack/solid-table' {
  interface ColumnMeta<
    TFeatures extends TableFeatures,
    TData extends RowData,
    TValue extends CellData = CellData,
  > {
    filterVariant?: 'text' | 'range' | 'select'
  }
  interface FilterFns {
    fuzzy: FilterFn<typeof stockFeatures, Person>
  }
  interface FilterMeta {
    itemRank?: RankingInfo
  }
}

const { createAppTable, createAppColumnHelper } = createTableHook({
  features: stockFeatures,
  rowModels: {
    expandedRowModel: createExpandedRowModel(),
    filteredRowModel: createFilteredRowModel({
      ...filterFns,
      fuzzy: (row, columnId, value, addMeta) => {
        const itemRank = rankItem(row.getValue(columnId), value)
        addMeta?.({ itemRank })
        return itemRank.passed
      },
    }),
    facetedRowModel: createFacetedRowModel(),
    facetedMinMaxValues: createFacetedMinMaxValues(),
    facetedUniqueValues: createFacetedUniqueValues(),
    groupedRowModel: createGroupedRowModel(aggregationFns),
    paginatedRowModel: createPaginatedRowModel(),
    sortedRowModel: createSortedRowModel(sortFns),
  },
})

const fuzzySort: SortFn<typeof stockFeatures, Person> = (
  rowA,
  rowB,
  columnId,
) => {
  let dir = 0
  if (rowA.columnFiltersMeta[columnId]) {
    dir = compareItems(
      rowA.columnFiltersMeta[columnId].itemRank!,
      rowB.columnFiltersMeta[columnId].itemRank!,
    )
  }
  return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir
}

const sortStatusFn: SortFn<typeof stockFeatures, Person> = (rowA, rowB) => {
  const statusOrder = ['single', 'complicated', 'relationship']
  return (
    statusOrder.indexOf(rowA.original.status) -
    statusOrder.indexOf(rowB.original.status)
  )
}

const getCommonPinningStyles = (
  column: Column<typeof stockFeatures, Person>,
): JSX.CSSProperties => {
  const isPinned = column.getIsPinned()
  const isLastLeftPinnedColumn =
    isPinned === 'left' && column.getIsLastColumn('left')
  const isFirstRightPinnedColumn =
    isPinned === 'right' && column.getIsFirstColumn('right')

  return {
    'box-shadow': isLastLeftPinnedColumn
      ? '-4px 0 4px -4px gray inset'
      : isFirstRightPinnedColumn
        ? '4px 0 4px -4px gray inset'
        : undefined,
    left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined,
    right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined,
    opacity: isPinned ? 0.97 : 1,
    position: isPinned ? 'sticky' : 'relative',
    'z-index': isPinned ? 1 : 0,
  }
}

function IndeterminateCheckbox(
  props: {
    indeterminate?: boolean
  } & JSX.InputHTMLAttributes<HTMLInputElement>,
) {
  let ref!: HTMLInputElement
  createEffect(() => {
    if (typeof props.indeterminate === 'boolean') {
      ref.indeterminate = !props.checked && props.indeterminate
    }
  })

  return <input type="checkbox" ref={ref} {...props} />
}

function DebouncedInput(
  props: {
    value: string | number
    onChange: (value: string | number) => void
    debounce?: number
  } & Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'>,
) {
  const [value, setValue] = createSignal(props.value)

  createEffect(() => {
    setValue(props.value)
  })

  createEffect(() => {
    const currentValue = value()
    const timeout = setTimeout(
      () => props.onChange(currentValue),
      props.debounce ?? 300,
    )
    onCleanup(() => clearTimeout(timeout))
  })

  return (
    <input
      {...props}
      value={value()}
      onInput={(e) => setValue(e.currentTarget.value)}
    />
  )
}

function Filter(props: { column: Column<typeof stockFeatures, Person> }) {
  const filterVariant = () => props.column.columnDef.meta?.filterVariant
  const sortedUniqueValues = createMemo(() =>
    filterVariant() === 'range'
      ? []
      : Array.from(props.column.getFacetedUniqueValues().keys())
          .sort()
          .slice(0, 5000),
  )

  return (
    <>
      {filterVariant() === 'range' ? (
        <div class="filter-row">
          <DebouncedInput
            type="number"
            min={Number(props.column.getFacetedMinMaxValues()?.[0] ?? '')}
            max={Number(props.column.getFacetedMinMaxValues()?.[1] ?? '')}
            value={
              (
                props.column.getFilterValue() as [number, number] | undefined
              )?.[0] ?? ''
            }
            onChange={(value) =>
              props.column.setFilterValue(
                (old: [number, number] | undefined) => [value, old?.[1]],
              )
            }
            placeholder={`Min${
              props.column.getFacetedMinMaxValues()?.[0] !== undefined
                ? ` (${props.column.getFacetedMinMaxValues()?.[0]})`
                : ''
            }`}
            class="filter-input"
          />
          <DebouncedInput
            type="number"
            min={Number(props.column.getFacetedMinMaxValues()?.[0] ?? '')}
            max={Number(props.column.getFacetedMinMaxValues()?.[1] ?? '')}
            value={
              (
                props.column.getFilterValue() as [number, number] | undefined
              )?.[1] ?? ''
            }
            onChange={(value) =>
              props.column.setFilterValue(
                (old: [number, number] | undefined) => [old?.[0], value],
              )
            }
            placeholder={`Max${
              props.column.getFacetedMinMaxValues()?.[1] !== undefined
                ? ` (${props.column.getFacetedMinMaxValues()?.[1]})`
                : ''
            }`}
            class="filter-input"
          />
        </div>
      ) : filterVariant() === 'select' ? (
        <select
          onChange={(e) => props.column.setFilterValue(e.currentTarget.value)}
          value={(props.column.getFilterValue() ?? '').toString()}
          class="filter-select"
        >
          <option value="">All</option>
          <For each={sortedUniqueValues()}>
            {(value) => <option value={String(value)}>{String(value)}</option>}
          </For>
        </select>
      ) : (
        <>
          <datalist id={props.column.id + 'list'}>
            <For each={sortedUniqueValues()}>
              {(value) => <option value={String(value)} />}
            </For>
          </datalist>
          <DebouncedInput
            type="text"
            value={(props.column.getFilterValue() ?? '') as string}
            onChange={(value) => props.column.setFilterValue(value)}
            placeholder={`Search (${props.column.getFacetedUniqueValues().size})`}
            class="filter-select"
            list={props.column.id + 'list'}
          />
        </>
      )}
    </>
  )
}

type AppTable = ReturnType<typeof createAppTable<Person>>

function TableHeader(props: {
  header: Header<typeof stockFeatures, Person, unknown>
  table: AppTable
}) {
  const column = () => props.header.column
  const style = () => ({
    ...getCommonPinningStyles(column()),
    'white-space': 'nowrap',
    width: `calc(var(--header-${props.header.id}-size) * 1px)`,
  })

  return (
    <th style={style()} colSpan={props.header.colSpan}>
      {!props.header.isPlaceholder ? (
        <>
          <div class="header-row">
            <div style={{ flex: 1, 'min-width': 0 }}>
              <div class="header-controls">
                {column().getCanPin() ? (
                  <span class="pin-actions">
                    {column().getIsPinned() !== 'left' ? (
                      <button
                        class="pin-button"
                        onClick={() => column().pin('left')}
                      >
                        {'<'}
                      </button>
                    ) : null}
                    {column().getIsPinned() ? (
                      <button
                        class="pin-button"
                        onClick={() => column().pin(false)}
                      >
                        x
                      </button>
                    ) : null}
                    {column().getIsPinned() !== 'right' ? (
                      <button
                        class="pin-button"
                        onClick={() => column().pin('right')}
                      >
                        {'>'}
                      </button>
                    ) : null}
                  </span>
                ) : null}
                {column().getCanGroup() ? (
                  <button
                    class="pin-button"
                    onClick={column().getToggleGroupingHandler()}
                  >
                    {column().getIsGrouped()
                      ? `Stop (${column().getGroupedIndex()})`
                      : 'Group'}
                  </button>
                ) : null}
              </div>
              {column().getCanSort() ? (
                <span
                  class="sortable-header"
                  onClick={column().getToggleSortingHandler()}
                >
                  <props.table.FlexRender header={props.header} />
                  {{
                    asc: ' â–²',
                    desc: ' â–¼',
                  }[column().getIsSorted() as string] ?? null}
                </span>
              ) : (
                <props.table.FlexRender header={props.header} />
              )}
              {column().getCanFilter() ? (
                <div>
                  <Filter column={column()} />
                </div>
              ) : null}
            </div>
          </div>
          {column().getCanResize() ? (
            <div
              onDblClick={() => column().resetSize()}
              onMouseDown={props.header.getResizeHandler()}
              onTouchStart={props.header.getResizeHandler()}
              class={`resizer ${column().getIsResizing() ? 'isResizing' : ''}`}
            />
          ) : null}
        </>
      ) : null}
    </th>
  )
}

function TableCell(props: {
  cell: Cell<typeof stockFeatures, Person, unknown>
  table: AppTable
}) {
  const className = () => {
    const groupingActive = props.table.atoms.grouping.get().length > 0
    const hasAggregation = !!props.cell.column.columnDef.aggregationFn
    return !groupingActive
      ? undefined
      : props.cell.getIsGrouped()
        ? 'cell-grouped'
        : hasAggregation && props.cell.getIsAggregated()
          ? 'cell-aggregated'
          : props.cell.getIsPlaceholder()
            ? 'cell-placeholder'
            : undefined
  }

  return (
    <td
      style={{
        ...getCommonPinningStyles(props.cell.column),
        width: `calc(var(--col-${props.cell.column.id}-size) * 1px)`,
      }}
      class={className()}
    >
      {props.cell.getIsGrouped() ? (
        <button
          onClick={props.cell.row.getToggleExpandedHandler()}
          style={{
            cursor: props.cell.row.getCanExpand() ? 'pointer' : 'normal',
          }}
        >
          {props.cell.row.getIsExpanded() ? 'v' : '>'}{' '}
          <props.table.FlexRender cell={props.cell} /> (
          {props.cell.row.subRows.length.toLocaleString()})
        </button>
      ) : (
        <props.table.FlexRender cell={props.cell} />
      )}
    </td>
  )
}

function PinnedRow(props: {
  row: Row<typeof stockFeatures, Person>
  table: AppTable
}) {
  const bottomRows = () => props.table.getBottomRows()
  return (
    <tr
      class="pinned-row"
      style={{
        position: 'sticky',
        top:
          props.row.getIsPinned() === 'top'
            ? `${props.row.getPinnedIndex() * 32 + 48}px`
            : undefined,
        bottom:
          props.row.getIsPinned() === 'bottom'
            ? `${(bottomRows().length - 1 - props.row.getPinnedIndex()) * 32}px`
            : undefined,
        'z-index': 1,
      }}
    >
      <For each={props.row.getVisibleCells()}>
        {(cell) => <TableCell cell={cell} table={props.table} />}
      </For>
    </tr>
  )
}

const columnHelper = createAppColumnHelper<Person>()

const columns = columnHelper.columns([
  columnHelper.display({
    id: 'select',
    size: 80,
    minSize: 80,
    maxSize: 80,
    enableSorting: false,
    enableGrouping: false,
    enableHiding: false,
    enableResizing: false,
    header: ({ table }) => (
      <IndeterminateCheckbox
        checked={table.getIsAllPageRowsSelected()}
        indeterminate={table.getIsSomePageRowsSelected()}
        onChange={table.getToggleAllPageRowsSelectedHandler()}
        title="Select all on this page"
      />
    ),
    cell: ({ row }) => (
      <div class="column-toggle-row">
        <IndeterminateCheckbox
          checked={row.getIsSelected()}
          disabled={!row.getCanSelect()}
          indeterminate={row.getIsSomeSelected()}
          onChange={row.getToggleSelectedHandler()}
        />{' '}
        <button
          class="pin-button"
          onClick={() => row.pin(row.getIsPinned() === 'top' ? false : 'top')}
        >
          {row.getIsPinned() === 'top' ? 'Pinned' : 'Pin'}
        </button>
      </div>
    ),
  }),
  columnHelper.accessor('firstName', {
    id: 'firstName',
    size: 200,
    header: 'First Name',
    filterFn: 'fuzzy',
    sortFn: fuzzySort,
    meta: { filterVariant: 'text' },
    getGroupingValue: (row) => `${row.firstName} ${row.lastName}`,
    cell: ({ row, getValue }) => (
      <div style={{ 'padding-left': `${row.depth * 1.5}rem` }}>
        {row.getCanExpand() ? (
          <button
            onClick={row.getToggleExpandedHandler()}
            style={{ cursor: 'pointer', 'margin-right': '0.25rem' }}
          >
            {row.getIsExpanded() ? 'v' : '>'}
          </button>
        ) : (
          <span style={{ 'margin-right': '0.25rem' }}>-</span>
        )}
        {String(getValue())}
      </div>
    ),
  }),
  columnHelper.accessor((row) => row.lastName, {
    id: 'lastName',
    size: 180,
    header: 'Last Name',
    meta: { filterVariant: 'text' },
  }),
  columnHelper.accessor('age', {
    id: 'age',
    size: 200,
    header: 'Age',
    meta: { filterVariant: 'range' },
    aggregationFn: 'median',
    aggregatedCell: ({ getValue }) =>
      Math.round(getValue<number>() * 100) / 100,
  }),
  columnHelper.accessor('visits', {
    id: 'visits',
    size: 200,
    header: 'Visits',
    meta: { filterVariant: 'range' },
    aggregationFn: 'sum',
    aggregatedCell: ({ getValue }) => getValue<number>().toLocaleString(),
  }),
  columnHelper.accessor('status', {
    id: 'status',
    size: 200,
    header: 'Status',
    sortFn: sortStatusFn,
    meta: { filterVariant: 'select' },
  }),
  columnHelper.accessor('progress', {
    id: 'progress',
    size: 200,
    header: 'Profile Progress',
    meta: { filterVariant: 'range' },
    aggregationFn: 'mean',
    cell: ({ getValue }) => `${Math.round(getValue<number>() * 100) / 100}%`,
    aggregatedCell: ({ getValue }) =>
      `${Math.round(getValue<number>() * 100) / 100}%`,
  }),
])

function App() {
  const [data, setData] = createSignal(makeData(1_000))
  const [, setRenderTick] = createSignal({})

  const table = createAppTable(
    {
      key: 'kitchen-sink', // needed for devtools
      columns,
      get data() {
        return data()
      },
      getSubRows: (row) => row.subRows,
      globalFilterFn: 'fuzzy',
      columnResizeMode: 'onChange',
      defaultColumn: { minSize: 200, maxSize: 800 },
      initialState: {
        columnOrder: columns.map((c) => c.id!),
        columnPinning: { left: ['select'], right: [] },
        pagination: { pageIndex: 0, pageSize: 20 },
      },
      keepPinnedRows: true,
      debugTable: true,
    },
    (state) => state,
  )

  useTanStackTableDevtools(table)

  const columnSizeVars = createMemo(() => {
    void table.atoms.columnResizing.get()
    void table.atoms.columnSizing.get()
    const colSizes: Record<string, number> = {}
    for (const header of table.getFlatHeaders()) {
      colSizes[`--header-${header.id}-size`] = header.getSize()
      colSizes[`--col-${header.column.id}-size`] = header.column.getSize()
    }
    return colSizes
  })

  const refreshData = () => setData(makeData(1_000))
  const nestedData = () => setData(makeData(100, 5, 3))
  const stress10k = () => setData(makeData(10_000))
  const stress100k = () => setData(makeData(100_000))
  const shuffleColumns = () => {
    table.setColumnOrder(
      faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)),
    )
  }

  return (
    <div class="demo-root">
      <h1>Kitchen Sink - All Features</h1>
      <div class="toolbar">
        <div class="toolbar-row">
          <DebouncedInput
            value={(table.atoms.globalFilter.get() ?? '') as string}
            onChange={(value) => table.setGlobalFilter(String(value))}
            class="global-filter-input"
            placeholder="Fuzzy search all columns..."
          />
        </div>
        <div class="toolbar-row">
          <button onClick={refreshData} class="demo-button demo-button-sm">
            Flat 1k
          </button>
          <button onClick={nestedData} class="demo-button demo-button-sm">
            Nested 100x5x3
          </button>
          <button onClick={stress10k} class="demo-button demo-button-sm">
            Stress 10k (flat)
          </button>
          <button onClick={stress100k} class="demo-button demo-button-sm">
            Stress 100k (flat)
          </button>
          <button
            onClick={() => table.reset()}
            class="demo-button demo-button-sm"
          >
            Reset Table
          </button>
          <button onClick={shuffleColumns} class="demo-button demo-button-sm">
            Shuffle Columns
          </button>
          <button
            onClick={() => setRenderTick({})}
            class="demo-button demo-button-sm"
          >
            Force Rerender
          </button>
          <span class="nowrap">
            {table.getSelectedRowModel().flatRows.length.toLocaleString()} of{' '}
            {table.getCoreRowModel().flatRows.length.toLocaleString()} selected
          </span>
        </div>
        <details class="column-toggle-panel">
          <summary class="column-toggle-panel-header">
            Column visibility
          </summary>
          <div class="column-toggle-row">
            <label>
              <input
                type="checkbox"
                checked={table.getIsAllColumnsVisible()}
                onChange={table.getToggleAllColumnsVisibilityHandler()}
              />{' '}
              Toggle All
            </label>
          </div>
          <For each={table.getAllLeafColumns()}>
            {(column) => (
              <div class="column-toggle-row">
                <label>
                  <input
                    type="checkbox"
                    checked={column.getIsVisible()}
                    disabled={!column.getCanHide()}
                    onChange={column.getToggleVisibilityHandler()}
                  />{' '}
                  {column.id}
                </label>
              </div>
            )}
          </For>
        </details>
      </div>
      <div class="table-container">
        <table
          style={{ ...columnSizeVars(), width: `${table.getTotalSize()}px` }}
        >
          <thead>
            <For each={table.getHeaderGroups()}>
              {(headerGroup) => (
                <tr>
                  <For each={headerGroup.headers}>
                    {(header) => <TableHeader header={header} table={table} />}
                  </For>
                </tr>
              )}
            </For>
          </thead>
          <tbody>
            <For each={table.getTopRows()}>
              {(row) => <PinnedRow row={row} table={table} />}
            </For>
            <For each={table.getCenterRows()}>
              {(row) => (
                <tr>
                  <For each={row.getVisibleCells()}>
                    {(cell) => <TableCell cell={cell} table={table} />}
                  </For>
                </tr>
              )}
            </For>
            <For each={table.getBottomRows()}>
              {(row) => <PinnedRow row={row} table={table} />}
            </For>
          </tbody>
        </table>
      </div>
      <div class="spacer-sm" />
      <div class="controls">
        <button
          class="demo-button demo-button-sm"
          onClick={() => table.setPageIndex(0)}
          disabled={!table.getCanPreviousPage()}
        >
          {'<<'}
        </button>
        <button
          class="demo-button demo-button-sm"
          onClick={() => table.previousPage()}
          disabled={!table.getCanPreviousPage()}
        >
          {'<'}
        </button>
        <button
          class="demo-button demo-button-sm"
          onClick={() => table.nextPage()}
          disabled={!table.getCanNextPage()}
        >
          {'>'}
        </button>
        <button
          class="demo-button demo-button-sm"
          onClick={() => table.setPageIndex(table.getPageCount() - 1)}
          disabled={!table.getCanNextPage()}
        >
          {'>>'}
        </button>
        <span class="inline-controls">
          <div>Page</div>
          <strong>
            {(table.atoms.pagination.get().pageIndex + 1).toLocaleString()} of{' '}
            {table.getPageCount().toLocaleString()}
          </strong>
        </span>
        <span class="inline-controls">
          | Go to page:
          <input
            type="number"
            min="1"
            max={table.getPageCount()}
            value={table.atoms.pagination.get().pageIndex + 1}
            onInput={(e) => {
              const page = e.currentTarget.value
                ? Number(e.currentTarget.value) - 1
                : 0
              table.setPageIndex(page)
            }}
            class="page-size-input"
          />
        </span>
        <select
          value={table.atoms.pagination.get().pageSize}
          onChange={(e) => table.setPageSize(Number(e.currentTarget.value))}
        >
          <For each={[10, 20, 30, 50, 100]}>
            {(pageSize) => <option value={pageSize}>Show {pageSize}</option>}
          </For>
        </select>
      </div>
      <div class="spacer-sm" />
      <div class="nowrap">
        {table.getRowModel().rows.length.toLocaleString()} rows on this page (
        {table.getFilteredRowModel().rows.length.toLocaleString()} filtered of{' '}
        {table.getCoreRowModel().rows.length.toLocaleString()} total)
      </div>
      <div class="spacer-md" />
      <details>
        <summary>Table state (live)</summary>
        <pre class="state-dump">
          {JSON.stringify(table.store.get(), null, 2)}
        </pre>
      </details>
    </div>
  )
}

export default App