Features Documentation

In-depth reference for every interactive feature built into TableCraft. Each section covers how the feature works internally, which config flag controls it, and any edge cases or server-side considerations to be aware of.

Sorting

Sorting allows users to order table rows by the values in any data column. The feature is enabled by default and requires no additional configuration beyond using DataTableColumnHeader in your column definitions.

How it works

  1. DataTableColumnHeader renders a dropdown button in each column header. The dropdown presents Sort Ascending and Sort Descending options, plus a Hide Column entry.
  2. Selecting a sort option calls column.toggleSorting(desc) on the TanStack Table instance, which updates the internal SortingState — an array of { id, desc } objects.
  3. The URL is updated to reflect the active sort: ?sort=columnId.asc or ?sort=columnId.desc. On mount, the component reads this param to restore the sorted state after navigation or page refresh.
  4. Visual indicators update on the header button: an ArrowUp icon for ascending, an ArrowDown icon for descending, and a neutral SortAsc icon when the column is not currently sorted.

Config flag

Set features.sorting: false in the provider or instance config to disable sorting globally. This sets enableSorting: false on the underlying TanStack Table instance, preventing all columns from being sorted.

Per-column control

To disable sorting for a specific column without touching the global flag, set enableSorting: false directly on the ColumnDef. That column will render a plain (non-interactive) header instead of a DataTableColumnHeader button, or the dropdown will omit the sort options if you still use DataTableColumnHeader.

Multi-column sort and server-side sorting
URL sync persists only the last active sort column. Multi-column sort state is supported by TanStack Table internally but will not survive a page reload. When any isQuery* flag is true, the table switches to manualSorting: true and will not reorder data locally — it expects pre-sorted data from the server on each fetch.

Filtering

TableCraft provides three filtering modes: client-side faceted filters that operate on in-memory data, server-side filters that delegate to a callback, and an advanced filter builder for complex multi-rule queries.

Client-Side Filtering

Client-side filtering operates entirely in the browser using TanStack Table's getFilteredRowModel(). Pass a filterableColumns array to enable it:

  • Faceted multi-select (DataTableFacetedFilter) — renders a command-palette popover with a search input and checkboxes. Multiple values can be selected simultaneously. Selected values are written to column.setFilterValue(array) and the URL is updated as ?columnId=value1.value2 (dot-separated). Active filters are displayed as dismissible badge chips in the toolbar's second row.
  • Single-select (DataTableSingleSelectFilter) — radio behavior with an "All" option that clears the filter. Set isSingleSelect: true on the DataTableFilterableColumn entry to use this variant. The popover closes automatically on selection.

Active filters can be cleared individually by clicking the badge's dismiss button, or all at once via the Reset button that appears in the filter row whenever at least one filter is active.

Server-Side Filtering

Enable server-side filtering with isQueryFilter={true}. In this mode the table instance is created with manualFiltering: true so no client-side row filtering is applied. Instead, each filter interaction calls your handleFilterChange(key, value) callback, which you use to update your query parameters and trigger a new data fetch.

Server-side filtering
<ClientSideTable
  data={data}
  columns={columns}
  pageCount={pageCount}
  isQueryFilter={true}
  handleFilterChange={(key, value) => {
    setFilters(prev => ({ ...prev, [key]: value }))
  }}
  filterableQuery={[
    { id: "status", title: "Status", options: [
      { label: "Active",   value: "active"   },
      { label: "Inactive", value: "inactive" },
    ]},
  ]}
  currentFilters={filters}
  onClearFilters={() => setFilters({})}
/>

Use currentFilters to pass the current filter state back into the toolbar so the active filter chips render correctly. Call onClearFilters to reset all filter state when the user clicks the toolbar Reset button.

Advanced Filtering

The advanced filter builder provides a per-column rule editor with operators and multi-rule logic. Enable it with isShowAdvancedFilter={true} or by setting features.advancedFilter: true in config. When active, the standard toolbar is replaced by DataTableAdvancedToolbar.

  • Operators — each filter rule supports contains, is, is not, and does not contain varieties, selectable via a dropdown on each filter pill (DataTableAdvancedFilterItem).
  • AND / OR logicDataTableMultiFilter lets users add, duplicate, and remove multiple rules for the same column, and toggle the logical operator between AND and OR.
  • URL format?columnId=value.variety, for example ?name=john.contains.

Pagination

The DataTablePagination footer renders page navigation, a page-size selector, and a record count summary. It adapts to a compact page / total format on mobile and uses a smart ellipsis algorithm on desktop to avoid rendering excessive page buttons.

Client-Side Pagination

By default, TableCraft uses TanStack Table's getPaginationRowModel() to split your static data array into pages in the browser. No server interaction is required.

  • Config options: pagination.defaultPageSize (default 10) sets the initial rows-per-page value. pagination.pageSizeOptions (default [10, 20, 30, 40, 50]) populates the page-size dropdown.
  • URL sync: Current page and page size are written to ?page=1&per_page=10 on every navigation so the user can share or bookmark a specific page view.

Server-Side Pagination

Enable server-side pagination with isQueryPagination={true}. The table renders pagination controls driven entirely by the metadata returned from your API. Page navigation and page-size changes invoke your callbacks rather than mutating internal state.

Server-side pagination
<ClientSideTable
  data={data}
  columns={columns}
  pageCount={meta.last_page}
  isQueryPagination={true}
  paginationData={{
    paginationResponse: {
      meta: {
        current_page: meta.current_page,
        last_page:    meta.last_page,
        per_page:     meta.per_page,
        total:        meta.total,
      },
    },
    onPageChange:     (page) => fetchData({ page }),
    onPageSizeChange: (size) => fetchData({ per_page: size }),
  }}
/>

The paginationResponse.meta object must provide current_page, last_page, per_page, and total. These values match the standard Laravel paginator response shape. The onPageChange(page) callback receives the 1-based page number; the onPageSizeChange(size) callback receives the new page size integer.

Cursor-Based Pagination

For APIs that use opaque cursors instead of page numbers (GraphQL Relay, Stripe, keyset pagination), enable cursor mode with isCursorPagination={true}. In this mode the pagination footer renders only Previous and Next buttons — no numbered page buttons or first/last jumps — because cursor-based APIs do not support random page access.

Cursor-based pagination
import type { CursorPaginationData } from "react-table-craft"

const cursorPaginationData: CursorPaginationData = {
  pageInfo: {
    hasNextPage:     pageInfo.hasNextPage,
    hasPreviousPage: pageInfo.hasPreviousPage,
    startCursor:     pageInfo.startCursor,
    endCursor:       pageInfo.endCursor,
    totalCount:      pageInfo.totalCount, // optional
  },
  onNextPage:       () => fetchMore({ after: pageInfo.endCursor }),
  onPreviousPage:   () => fetchMore({ before: pageInfo.startCursor }),
  onPageSizeChange: (size) => fetchMore({ first: size }),
  pageSize:         20,
}

<ClientSideTable
  data={edges.map(e => e.node)}
  columns={columns}
  isCursorPagination={true}
  cursorPaginationData={cursorPaginationData}
/>

The cursorPaginationData object provides a pageInfo shape with hasNextPage and hasPreviousPage booleans that control button state, optional startCursor / endCursor strings for your fetch callbacks, and an optional totalCount that, when provided, renders a "N records" label. The pageSize field controls the page-size selector's initial value.

URL sync is disabled in cursor mode
When isCursorPagination is active, the ?page and ?per_page URL parameters are not written because page numbers have no meaning in cursor-based pagination. Sorting URL sync still works normally.

Row Selection

Row selection adds a checkbox column that lets users select individual rows or all rows on the current page. Selected rows can then be acted on via the floating bar, the toolbar delete button, or your own bulk-action logic.

Enable the feature with features.rowSelection: true in config. Row selection is defined in your column definitions, not inside TableCraft itself, which gives you full control over checkbox rendering, aria labels, and disabled states.

Row selection column definition
import { ColumnDef } from "@tanstack/react-table"
import { Checkbox } from "@/components/ui/checkbox"

const columns: ColumnDef<User>[] = [
  {
    id: "select",
    header: ({ table }) => (
      <Checkbox
        checked={table.getIsAllPageRowsSelected()}
        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
        aria-label="Select all"
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(value) => row.toggleSelected(!!value)}
        aria-label="Select row"
      />
    ),
    enableSorting: false,
    enableHiding:  false,
  },
  // ... remaining columns
]

The reserved column id "select" is recognized by the column-visibility toggle and is excluded from the hide/show list automatically. When features.rowSelection is false, TanStack Table sets enableRowSelection: false on the instance — checkboxes may still render if you include the column definition, but toggling them produces no state changes.

Column Visibility

The column visibility toggle renders as a Columns dropdown button in the toolbar. It lists all data columns (those with an accessorFn or accessorKey where getCanHide() returns true) with checkboxes. Toggling a checkbox calls column.toggleVisibility(), which hides or shows that column instantly without a re-fetch.

Display columns with ids "select" and "actions" are excluded from the list. Set features.columnVisibility: false to hide the toggle button entirely and keep all columns permanently visible. Column visibility state is not currently synced to the URL.

CSV Export

The CSV export button downloads the currently selected rows as a .csv file. The button is disabled when no rows are selected, providing a clear affordance that selection is a prerequisite.

Props

PropTypeRequiredDefaultDescription
isShowExportButtons.isShowbooleanYesShow or hide the export button in the toolbar.
isShowExportButtons.ignoredColsstring[]NoColumn ids to exclude from the CSV output. The 'actions' and 'select' columns are always excluded regardless of this setting.
isShowExportButtons.fileNamestringNotable-exportBase filename for the downloaded CSV file (without extension).

Behavior notes

  • Uses the export-to-csv library for file generation and triggers a browser download with a synthetic anchor click.
  • Cell values are extracted from the visible cells of selected rows. Null, undefined, and empty string values are skipped.
  • CSV column headers are derived from the column header text when available, falling back to the column's id.
  • Enable the button by setting features.csvExport: true in config (it is enabled by default) and passing isShowExportButtons.isShow: true.
Export all vs. export selected
The current implementation exports only selected rows. If you need to export all rows regardless of selection, select all rows first using the header checkbox, then click Export.

View Toggle

The view toggle lets users switch between the default table layout and a responsive card grid layout. The toggle buttons appear in the top-left corner of the toolbar when features.viewToggle is true and the parent component supplies both viewMode and onViewModeChange props.

Card view details: cards are laid out in a responsive grid — 1 column on mobile, 2 columns at the sm breakpoint, and 3 columns at lg. Each card displays the row selection checkbox alongside the row number, followed by all visible data columns rendered as label: value pairs. When a card's row is selected, it receives a primary-color border and a subtle background tint to make the selection state obvious without relying on the checkbox alone.

Both views share the same TanStack Table instance and the same toolbar controls — sorting, filtering, pagination, and row selection work identically regardless of which view is active.

Floating Bar

The floating bar is a fixed-position action strip that slides in at the bottom of the viewport whenever at least one row is selected. It provides quick access to bulk actions without requiring the user to scroll back up to the toolbar.

Config flag: features.floatingBar — disabled by default. Enable it by setting this flag to true in provider or instance config, or passfloatingBar={true} directly to DataTable.

Visibility condition: The bar renders only when table.getFilteredSelectedRowModel().rows.length > 0. It disappears automatically when the selection is cleared.

Available actions:

  • Clear selection — an X button that calls table.resetRowSelection() and dismisses the bar.
  • Selection count — a read-only label showing how many rows are currently selected (e.g., 3 row(s) selected).
  • Delete — rendered only when deleteRowsAction is provided. Calls the supplied MouseEventHandler with the click event so you can perform your own confirmation dialog and data mutation.
Floating bar vs. toolbar delete
Both the floating bar and the standard toolbar can display a delete button driven by the same deleteRowsAction prop. The floating bar is the recommended pattern for bulk-delete UX because it remains visible regardless of scroll position and clearly communicates the number of affected rows.