npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chainsys/sab-react-grid

v2.2.0

Published

Enterprise React data grid (TanStack Table): virtualization, filters, facet search, grouping, matrix & pivot comparison views, frozen columns, aggregations, field navigation, Intl currency / URL / rich-text formatters, Excel & CSV export, Tailwind UI and

Readme

@chainsys/sab-react-grid

A professional, enterprise-grade data grid for React applications. Built on TanStack Table and TanStack React Virtual, it delivers high-performance tables with sorting, filtering, grouping, column visibility, virtualization, and Excel/CSV export. The UI is Tailwind CSS–based (utility classes and dark: variants), with light / dark / system theme handling that integrates with your app’s toggle or document-level styling.

Package: @chainsys/sab-react-grid
Version: 2.2.0
License: MIT

Technical documentation: On the npm package page, only this README.md is rendered as formatted Markdown. The full technical reference is included in this same document below (Technical documentation) so it uses the same preview as the rest of the readme.


Features

| Feature | Description | |--------|-------------| | TanStack Table | Full control over sorting, filtering, grouping, pagination, column visibility, order, and sizing | | Row virtualization | Renders only visible rows via @tanstack/react-virtual for smooth scrolling with large datasets | | Layout modes | fit-default, fit-window, or fit-content (content-based column widths) | | Column filters | Text, number, date, datetime, time (Flatpickr), multiselect, checkbox, boolean, dropdown, and radio with operand support (contains, equals, greater than, etc.) | | Filter placeholders | Search icon (🔎︎) for text/number and dropdown filters; calendar icon for date/datetime/time filters (with SVG placeholder when empty) | | Export | Excel (.xlsx) via ExcelJS and CSV via plain Blob from the toolbar Data Export menu | | Grouping | Drag columns into the grouping zone; expand/collapse groups | | Aggregations (group + list footers) | Optional per-column aggregates with Slickgrid-style group footers (under expanded groups) and an optional list footer (<tfoot>) for grand totals | | Column reorder | Drag-and-drop column reordering in the header | | Column visibility | Show/hide columns from the Columns menu | | Sticky headers | Header and filter rows stay fixed while the body scrolls | | Row selection | Opt-in checkbox selector column (header select-all + per-row checkbox) with onRowSelectionChange callback | | Actions column | Row actions (Add, View, Edit, Delete, List, or custom) with configurable icon, label, and navigation URL from meta.actions | | Builtin navigation for actions | Actions can reuse the same built-in router/dialog flow as field navigation (no app callback required) | | Performance | Memoized header, filter, row, and cell components | | Tailwind UI | Toolbar, filters, cells, and chrome styled with Tailwind utilities; optional built-in toolbar light/dark toggle | | Theme API | theme, showThemeToggle, and onThemeChange for controlled or document-driven (system) appearance; root uses data-sab-theme for debugging and CSS hooks | | Tailwind content helper | Subpath export @chainsys/sab-react-grid/tailwind-content so your build scans the package and generates all grid classes (avoids “unstyled” or wrong-mode cells after purge) | | Card styles (fields & actions) | Per-column card/frame and typography via meta.labelStyle, meta.boxStyle, meta.valueStyle; per-action buttonStyle / labelStyle on TableAction | | Frozen columns | Column header menu: Freeze Upto (pin left through selected column) and Unfreeze all columns; optional columnDef.enablePinning: false to exclude a column from freeze | | Field navigation (click & hover) | meta.fieldNavigationInfo: click activates on pointer/keyboard; hover uses a short dwell timer before firing — header glyph + cell data-sab-* attributes | | Currency columns | fieldType: 'currency' (or legacy dataType) with formatOptions.currencySymbol and formatOptions.decimals; locale-aware number grouping in DataFormatter and plain-text export via formatCellValueAsPlainText | | URL columns | fieldType: 'url': chip links for { displayValue?, urlValue }, arrays, or strings; +N overflow in a fixed portal; filters match urlValue only; CSV/Excel export emits comma-separated URLs | | Rich text columns | fieldType: 'richtext': list cell shows View Detail with sanitized HTML (RichTextFormatter / DOMPurify); optional RichtextHeaderIcon in the header; export strips tags and media for readable plain text | | Facet search | Horizontal chip workspace + column side-panel facets; client-side in-memory filtering or server fetch via onFacetClick (selection only); host-supplied counts via facetCountsByField + computeFacetCountsByField | | Matrix / pivot comparison | Predefined (matrixConfigJSON, pivotConfigJSON) or Interactive (enableInteractiveMatrix, enableInteractivePivot) comparison views; pivot-only footerTotals; row-axis column hidden from Columns menu and auto-frozen; column resize in comparison mode | | Server pagination callbacks | Dedicated onPageChange and onItemsPerPageChange (each may return facetCountsByField); pair with totalRowCount and currentPage for parent-controlled paging — independent from facet selection |

Card styles (fields & actions)

Use inline React CSS objects on column meta so JSON-driven configs can style cells without custom cell renderers:

| Key | Where it applies | Purpose | |-----|------------------|---------| | labelStyle | Header and filter cells | Chrome for the title/filter row: background, borders, radius (not body “cards”) | | boxStyle | Body value cells | Card / frame: backgroundColor, borderRadius, borders, boxShadow — applied on the cell chrome | | valueStyle | Body value content | Typography / presentation: color, fontWeight, textDecoration — frame keys are stripped and should live in boxStyle |

Actions: each entry in meta.actions can set buttonStyle, labelStyle, className, iconClassName for per-button styling.

// Data column: soft card + bold value
meta: {
  dataType: 'currency',
  boxStyle: { backgroundColor: '#f8fafc', borderRadius: 8, border: '1px solid #e2e8f0' },
  valueStyle: { fontWeight: 600, color: '#0f172a' },
},
// Actions: compact primary button
meta: {
  actions: [
    {
      id: 'edit',
      label: 'Edit',
      icon: 'Edit',
      buttonStyle: { backgroundColor: '#4f46e5', color: '#fff', borderRadius: 6, padding: '4px 10px' },
      labelStyle: { fontWeight: 600 },
    },
  ],
},

Frozen columns

  • Open the column header chevron (⋮) menu on any column that allows pinning.
  • Freeze Upto — pins all visible leaf columns from the left through this column (inclusive). The grid validates the request (for example you cannot freeze through the last visible column; viewport hints may appear as toasts).
  • Unfreeze all columns — clears the custom freeze and returns to normal horizontal scroll.
  • To disable freeze for a specific column (no “Freeze Upto” effect on that column), set TanStack columnDef.enablePinning: false.

Internally the grid may use sticky pinning or a split layout (separate frozen vs scroll panes) depending on state; behavior is the same from the user’s perspective: left columns stay visible while the rest scrolls horizontally.

Field navigation: click vs hover

Configure meta.fieldNavigationInfo on data columns (default DataFormatter / date cells only — custom column.cell is not wrapped). The column header shows a small glyph and tooltip (“field click” vs “field hover”). Data attributes on the cell wrapper help tests and theming: data-sab-field-interaction, data-sab-field-id, data-sab-column-id.

| triggerEvent | When navigation runs | UX notes | |----------------|------------------------|----------| | click | On click (and Enter / Space when focused) | cursor-pointer, role="button", tabIndex={0}; click does not bubble to the row | | hover | After the pointer stays over the value ~400ms (mouseenter → timer; cleared on mouseleave) | Avoids accidental navigation while moving the mouse; native title on the value can show the formatted cell string |

For both modes, handling follows type (router vs dialog) and fieldNavigationBehavior (builtin vs callback). Use onFieldNavigation only for callback; builtin dialog uses renderFieldNavigationDialog or builtinDialogPreset.

Minimal click (builtin router):

meta: {
  fieldNavigationInfo: {
    id: 'region-nav',
    triggerEvent: 'click',
    type: 'router',
    navigationUrl: (row) => `/regions/${(row as { id: string }).id}`,
  },
},

Minimal hover (callback — app handles preview):

<SabReactTable
  data={rows}
  columns={columns}
  onFieldNavigation={(payload) => {
    if (payload.triggerEvent !== 'hover') return
    // open preview / tooltip app using payload.row, payload.cellValue, payload.fieldId
  }}
/>

// column meta:
meta: {
  fieldNavigationInfo: {
    id: 'preview-units',
    triggerEvent: 'hover',
    type: 'dialog',
    fieldNavigationBehavior: 'callback',
  },
},

Installation

npm install @chainsys/sab-react-grid @tanstack/react-table @tanstack/react-virtual react react-dom

Peer dependencies

Install in your application if not already present:

  • react (^18.0.0 or ^19.0.0)
  • react-dom (^18.0.0 or ^19.0.0)
  • @tanstack/react-table (^8.0.0)
  • @tanstack/react-virtual (^3.0.0)
  • flatpickr and react-flatpickr (date/datetime/time filters)
  • tailwindcss (^4.2.2) — recommended whenever you style the app with Tailwind; marked optional in peerDependenciesMeta so non-Tailwind consumers can still install the package, but the grid’s classes require Tailwind in the host build for correct appearance

Dependencies (installed automatically)

  • date-fns – date formatting
  • exceljs – Excel (.xlsx) export (larger files; NPM vulnerabilities mitigated via package overrides)
  • flatpickr, react-flatpickr – date/datetime/time filters

CSV export uses no extra libraries (plain string/Blob).

Tailwind CSS and theming

The component library does not ship a separate CSS bundle: styling is Tailwind utility classes (bg-*, text-*, borders, dark:*, etc.). Your application must run Tailwind v4 (or compatible v3 with dark: variant support) in its build and include this package’s sources in Tailwind content, or unused classes are purged and the grid can look unstyled or stuck in light mode while the rest of the app is dark.

Subpath export: tailwind-content

The package publishes @chainsys/sab-react-grid/tailwind-content, which resolves to absolute globs for dist/**/*.{js,mjs,cjs} and src/**/*.{ts,tsx} inside the installed package. Spread it into content so paths work in monorepos and different node_modules layouts.

ESM tailwind.config.js (recommended):

import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const sabGridContent = require('@chainsys/sab-react-grid/tailwind-content')

export default {
  darkMode: 'selector', // or 'class'; align with how you set `dark` on `<html>` / `:root`
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}', ...sabGridContent],
}

CommonJS tailwind.config.cjs:

const sabGridContent = require('@chainsys/sab-react-grid/tailwind-content')
module.exports = {
  darkMode: 'selector',
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}', ...sabGridContent],
}

Important: If your Tailwind config sets content, it replaces any preset’s content entirely. Do not assume a shared preset alone will scan this package—you must merge ...sabGridContent (or equivalent manual globs) into your app’s content array.

Manual globs (only if you cannot use the subpath; more brittle):

content: [
  './index.html',
  './src/**/*.{js,ts,jsx,tsx}',
  './node_modules/@chainsys/sab-react-grid/dist/**/*.{js,mjs,cjs}',
  './node_modules/@chainsys/sab-react-grid/src/**/*.{ts,tsx}',
],

Install tailwindcss, postcss, and autoprefixer in the app and load a global stylesheet with @tailwind directives.

Theme behavior (theme, showThemeToggle, onThemeChange)

| Mode | How it works | |------|----------------| | Controlled | Pass theme="light" or theme="dark" from the same state you use for the rest of the app (e.g. next-themes, Zustand, or useState). The grid follows your value even if <html> differs; it applies Tailwind’s dark class on the grid root when needed. | | Uncontrolled | Omit theme (or use theme="system"). The grid starts in system mode: it observes class / data-theme on documentElement, body, and #root so it tracks <html class="dark">, data-theme="dark", etc. | | Toolbar toggle | Set showThemeToggle to show a sun/moon control. If theme is controlled, you must pass onThemeChange so toolbar clicks update your app state (and typically <html class="dark">). Otherwise the button cannot sync the grid with your app. |

Example (controlled, matches a typical app toggle):

const [dark, setDark] = useState(false)

return (
  <>
    <button type="button" onClick={() => setDark((d) => !d)}>
      Toggle theme
    </button>
    <SabReactTable
      data={data}
      columns={columns}
      theme={dark ? 'dark' : 'light'}
      showThemeToggle
      onThemeChange={(t) => setDark(t === 'dark')}
    />
  </>
)

The grid root exposes data-sab-theme="light" | "dark" for debugging and optional host CSS. Types: SabGridTheme ('light' | 'dark' | 'system'), SabGridResolvedTheme ('light' | 'dark' for callbacks).


Project structure and main files

| Path | Purpose | |------|---------| | src/index.ts | Package entry; re-exports component, formatters, export helpers, and types | | src/SabReactTable.tsx | Main grid component: table UI, toolbar, filter row, virtualization, grouping, export menu | | src/utils/tableFormatters.tsx | DataFormatter, ActionFormatter, date formatting, TableAction / ACTION_ICON_KEYS / ActionIcons | | src/utils/exportUtils.ts | exportTableToExcel (ExcelJS), exportTableToCSV (plain Blob) | | src/utils/sabFacetSearch.ts | Facet types, config resolution, row value helpers | | src/utils/facet/FacetConfigBuilder.ts | Build runtime facet UI configuration from host JSON | | src/utils/facet/FacetCountEngine.ts | Client-side exclude-field facet count calculation | | src/utils/facet/FacetFilterEngine.ts | OR-within / AND-across facet row filtering | | src/utils/facet/getClientFilteredData.ts | Client-mode filter; isHostSuppliedServerPage for server page slices; deprecated fetch-mode paging helpers | | src/components/grid/facet/ | Facet workspace UI (FacetPanel, chips, carousel, clear tab) | | src/components/grid/ColumnFacetSearchSidePanel.tsx | Searchable column facet side panel | | src/components/grid/SabGridSidePanel.tsx | Right-docked slide panel shell (facet side panel) | | dist/ | Built output (ESM + CJS + types) | | tailwind-content.cjs | Published alongside dist for Tailwind content globs (exports["./tailwind-content"]) |

Build: npm run build (runs clean then build:esm and build:cjs). The prepublishOnly script runs the same build before publish.
Clean: npm run clean removes dist before build to avoid stale artifacts.


Facet search

Facet search adds a workspace chip bar above the grid (low-cardinality fields) and optional column side panels (text or high-cardinality fields). Selection logic is OR within a field, AND across fields. Column toggle filters remain independent and stack on top of facet filtering.

Configuration

Pass facetConfig as either a field array or a top-level JSON object:

<SabReactTable
  data={rows}
  columns={columns}
  facetConfig={{
    enableInternalFacetFilter: true, // default when not server-paginated
    FacetConfigurationJSON: [
      {
        field: 'status',
        label: 'Status',
        fieldType: 'select',
        showZeroCount: false, // zero-count chips visible but read-only
        options: [
          { value: 'active', label: 'Active' },
          { value: 'inactive', label: 'Inactive' },
        ],
      },
      {
        field: 'department',
        label: 'Department',
        fieldType: 'multiselect',
        showZeroCount: true, // zero-count chips stay clickable (default)
        options: [...],
      },
    ],
  }}
/>

Per-field options (FacetConfigurationJSON[])

Each entry is a FacetConfigEntry:

| Property | Type | Default | Description | |----------|------|---------|-------------| | field | string | — | Column accessor / field id (required) | | label | string | Humanized field | Category label shown before chips (Status ›) | | fieldType | 'select' \| 'radio' \| 'dropdown' \| 'multiselect' \| 'checkbox' \| 'text' | 'text' | Drives matching logic and layout auto-pick | | facetDisplay | 'chips' \| 'sidePanel' | Auto (≤8 options → chips; text / many options → side panel) | Force workspace chips vs column side panel | | showZeroCount | boolean | true | Controls clickability of zero-count options (see below) — options are always shown | | options | { value, label }[] | — | Static option list for selection fields; counts filled at runtime | | style | FacetConfigStyle | DEFAULT_FACET_STYLE | Per-field chip colors (labelColor, valueColor, backgroundColor, valueFont, countColor) |

Top-level FacetTableConfig (when not passing a bare array):

| Property | Type | Default | Description | |----------|------|---------|-------------| | enableInternalFacetFilter | boolean | true when not server-paginated | true = filter data in memory; false = host refetch via onFacetClick | | FacetConfigurationJSON | FacetConfigEntry[] | — | Facet field definitions |

| fieldType | Selection behavior | |-------------|-------------------| | select, radio, dropdown, multiselect, checkbox | Multiple chips per field (OR within field); AND across fields | | text (or omitted) | Side-panel facet with search box; values derived from data |

Layout: omit facetDisplay in most configs — the grid auto-picks chips (≤8 options) vs sidePanel (text or many options).

Zero-count options (showZeroCount)

Facet option counts are recomputed whenever filters change (client mode or after onFacetClick). Options with count 0 are always rendered in both the horizontal chip bar and the column side panel — they are never hidden.

The per-field showZeroCount flag controls whether a zero-count option can be selected:

| showZeroCount | Zero-count option (not yet selected) | Already-selected zero-count chip | |-----------------|--------------------------------------|----------------------------------| | true (default) | Visible and clickable | Clickable (can deselect) | | false | Visible but read-only — greyed out, cursor-not-allowed, no click/keyboard activation | Clickable (can deselect) |

Components: enforced in FacetChip (workspace chips), FacetInlineGroup (passes the flag per field), and ColumnFacetSearchSidePanelBody (checkbox rows in the side panel).

// Allow users to select a facet value even when its count is currently 0
{ field: 'region', fieldType: 'select', showZeroCount: true, options: [...] }

// Show zero-count values for context but prevent selecting them
{ field: 'tier', fieldType: 'select', showZeroCount: false, options: [...] }

Use showZeroCount: false when you want users to see that an option exists but has no matching rows in the current filter context (e.g. server fetch where counts are derived from returned rows).

Client mode (enableInternalFacetFilter: true)

  • Pass the full dataset in data.
  • The grid filters rows in memory and recomputes chip counts using exclude-field refinement (counts for field A ignore selections on field A).
  • onFacetClick is optional (notification only if provided).

Server fetch mode (enableInternalFacetFilter: false)

Host pre-loads the first page (recommended). Facet selection and pagination use separate callbacks:

  • onFacetClick — fires on chip click or Clear (reason: 'selection' only). Host refetches, updates data / totalRowCount / currentPage, and should return { facetCountsByField } when data is a page slice.
  • onPageChange / onItemsPerPageChange — toolbar prev/next and page-size changes. Each may return { facetCountsByField } to refresh chip counts without recounting the page body.
const [rows, setRows] = useState<User[]>([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const activeFiltersRef = useRef<ActiveFacetFilters>({})
const facetCountsCacheRef = useRef<Record<string, FacetOption[]> | null>(null)

// Pre-load page 1 before mount
useEffect(() => {
  api.listUsers({ page: 1, pageSize: 20 }).then((res) => {
    setRows(res.items)
    setTotal(res.total)
    facetCountsCacheRef.current = computeFacetCountsByField(
      res.items, facetEntries, columns, {}
    )
  })
}, [])

<SabReactTable
  data={rows}
  totalRowCount={total}
  currentPage={page}
  facetConfig={{ enableInternalFacetFilter: false, FacetConfigurationJSON: [...] }}
  onFacetClick={async (payload) => {
    activeFiltersRef.current = payload.activeFilters
    const res = await api.listUsers({
      filters: payload.activeFilters,
      page: 1,
      pageSize: 20,
    })
    setRows(res.items)
    setTotal(res.total)
    setPage(1)
    facetCountsCacheRef.current = computeFacetCountsByField(
      res.items, facetEntries, columns, payload.activeFilters
    )
    return { facetCountsByField: facetCountsCacheRef.current }
  }}
  onPageChange={async ({ page: nextPage }) => {
    setPage(nextPage)
    const res = await api.listUsers({
      filters: activeFiltersRef.current,
      page: nextPage,
      pageSize: 20,
    })
    setRows(res.items)
    return facetCountsCacheRef.current
      ? { facetCountsByField: facetCountsCacheRef.current }
      : undefined
  }}
  onItemsPerPageChange={async (pageSize) => {
    setPage(1)
    const res = await api.listUsers({
      filters: activeFiltersRef.current,
      page: 1,
      pageSize,
    })
    setRows(res.items)
    setTotal(res.total)
    facetCountsCacheRef.current = computeFacetCountsByField(
      res.items, facetEntries, columns, activeFiltersRef.current
    )
    return { facetCountsByField: facetCountsCacheRef.current }
  }}
/>

| Return field | When | Purpose | |--------------|------|---------| | facetCountsByField | Server page slices | Precomputed exclude-field counts from the full filtered corpus — grid applies via internal hostFacetCountsByField | | rows / totalRowCount | Optional legacy | Prefer updating data / totalRowCount / currentPage props directly |

onFacetClick payload (FacetClickPayload)

| Field | Type | Description | |-------|------|-------------| | reason | 'selection' | Always selection — pagination uses onPageChange / onItemsPerPageChange | | enableInternalFacetFilter | boolean | true = client filter mode; false = fetch mode (host refetches) | | activeFilters | ActiveFacetFilters | Full facet selection after the click (Record<field, string[]>) | | field / value / action | — | Chip metadata (select | deselect | clear) | | filteredRows | TData[] | Rows after facet filter (client mode) or current data (fetch mode) | | originalRows | TData[] | Current data prop | | facetCounts | FacetCountResult[] | Grid-computed counts before host response | | page / pageSize | number | Context at time of selection |

Pagination integration

Four patterns work together with facets:

| Pattern | Props | Facet interaction | |---------|-------|-------------------| | A — Client pagination | Default TanStack paging on filtered data | Facet filter resets page index via autoResetPageIndex | | B — Server page navigation | totalRowCount, currentPage, onPageChange | Host refetches on prev/next (hostPageNavigation); facet selection via onFacetClick | | C — Buffered internal paging | onItemsPerPageChange only; larger data buffer | Host loads pageSize + buffer; prev/next paginate internally within loaded rows | | D — Full server | Case B + onItemsPerPageChange | Host refetches on every toolbar navigation |

SabPageChangeInfo: { page: number, direction: 'prev' | 'next' }page is 1-based (matches toolbar display).

All pagination callbacks may return { facetCountsByField } — use computeFacetCountsByField on the host when isHostSuppliedServerPage(data, totalRowCount) is true.

Facet selection in fetch mode resets to page 1 automatically.

Styling

Per-field chip colors via facetConfig[].style (labelColor, valueColor, backgroundColor, valueFont, countColor). Defaults match Tailwind indigo workspace styling (DEFAULT_FACET_STYLE).


Quick start

import { SabReactTable } from '@chainsys/sab-react-grid'
import type { ColumnDef } from '@tanstack/react-table'

interface Person {
  id: number
  name: string
  email: string
  role: string
}

const columns: ColumnDef<Person, unknown>[] = [
  { id: 'id', accessorKey: 'id', header: 'ID' },
  { id: 'name', accessorKey: 'name', header: 'Name' },
  { id: 'email', accessorKey: 'email', header: 'Email' },
  { id: 'role', accessorKey: 'role', header: 'Role', meta: { dataType: 'text' } },
]

const data: Person[] = [
  { id: 1, name: 'Alice', email: '[email protected]', role: 'Admin' },
  { id: 2, name: 'Bob', email: '[email protected]', role: 'User' },
]

export function MyTable() {
  return (
    <SabReactTable<Person>
      data={data}
      columns={columns}
      title="Users"
      defaultPageSize={50}
      pageSizeOptions={[10, 20, 50, 100]}
      initialLayoutMode="fit-content"
      onSortedDataChange={(rows) => console.log('Filtered/sorted rows', rows)}
      onRowClick={(row) => console.log('Clicked', row)}
    />
  )
}

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | data | TData[] | required | Row data | | columns | ColumnDef<TData, any>[] | required | TanStack Table column definitions | | title | string | 'Table_List' | Table title in the toolbar | | onSortedDataChange | (rows: TData[]) => void | — | Called with filtered/sorted rows (pre-pagination) | | onPageChange | (pageInfo: SabPageChangeInfo) => void \| Promise<FacetClickResult> | — | Fired after toolbar prev/next. page is 1-based; includes direction: 'prev' \| 'next'. May return { facetCountsByField } | | onItemsPerPageChange | (pageSize: number) => void \| Promise<FacetClickResult> | — | Fired after items-per-page dropdown change. May return { facetCountsByField } | | totalRowCount | number | — | Server-side pagination: total rows across all pages. Pair with currentPage and onPageChange | | currentPage | number | — | 1-based active page when using totalRowCount (syncs toolbar after parent refetch) | | facetConfig | FacetConfigInput | — | Table-level facet field definitions (array or { enableInternalFacetFilter, FacetConfigurationJSON }). Required to show facet UI | | onFacetClick | (payload) => void \| Promise<FacetClickResult> | — | Fetch mode (enableInternalFacetFilter: false): fires on facet selection only. Host updates data / totalRowCount / currentPage; return { facetCountsByField } for server page slices | | defaultPageSize | number | 50 | Initial page size | | pageSizeOptions | number[] | [10, 20, 50, 100, 500, 1000] | Page size dropdown options | | groupingLabels | Record<string, string> | {} | Display labels for grouping columns | | onRowClick | (row: TData) => void | — | Row click handler | | onActionClick | (actionId, row, action) => void | — | Handler for action buttons; use action.navigate or action.id to route | | onFieldNavigationOverride | (args) => boolean \| void | — | Optional override hook; return true to stop built-in navigation when column meta.fieldNavigationInfo is set | | onFieldNavigation | (payload) => void | — | callback columns only — navigate or app-owned dialog. Not called for builtin + dialog (use renderFieldNavigationDialog) | | routerNavigate | SabRouterNavigateFn | — | Optional override for built-in router navigation; omit when the table is under react-router’s Router (wired automatically) | | routerLocation | { pathname; search; hash } | — | Router location snapshot (only needed when you pass a custom routerNavigate and use preserveLocation) | | renderFieldNavigationDialog | (args) => ReactNode | — | Builtin type: 'dialog' body inside the grid modal; onFieldNavigation is not used for this path | | initialLayoutMode | 'fit-default' \| 'fit-window' \| 'fit-content' | 'fit-content' | Initial layout mode | | theme | SabGridTheme | — | 'light', 'dark', or 'system' (follows document dark / data-theme). Omit for uncontrolled system mode. | | showThemeToggle | boolean | false | Toolbar sun/moon control; pair with onThemeChange when theme is controlled. | | onThemeChange | (theme: SabGridResolvedTheme) => void | — | Fires when the user changes theme via the toolbar; update app state and document class when using controlled theme. | | fillParent | boolean | false | If true, grid uses flex-1 min-h-0 so it fills a flex parent and scrolls within available space | | className | string | — | Extra classes on the root grid card | | getRowId | (row, index) => string | — | Stable row id for selection when rows lack a unique id field. | | onRowSelectionChange | (selection, selectedRows) => void | — | TanStack RowSelectionState and selected data rows when row selection is used. | | toolbarIcons | SabGridToolbarIcons | — | Replace default toolbar glyphs (title badge, command menu, filter toggle, pagination prev/next) |


Column meta

Use the meta property on column definitions to control formatting and filtering.

Column kind resolution: the grid uses resolveColumnFieldKind(meta) everywhere it used to read dataType alone: meta.fieldType first, then meta.dataType if fieldType is unset. Hosts can migrate configs gradually; fieldType overrides dataType when both are present.

| Meta key | Type | Description | |----------|------|-------------| | fieldType | 'text' \| 'number' \| 'decimal' \| 'currency' \| 'email' \| 'boolean' \| 'date' \| 'datetime' \| 'time' \| 'percentage' \| 'richtext' \| 'url' | Preferred column kind for formatters, filters, alignment, and export | | dataType | Same union as fieldType | Legacy alias of fieldType. If both are set, fieldType wins (resolveColumnFieldKind in source) | | labelStyle | React.CSSProperties | Header / filter cell chrome (background, borders) — not body card frame | | boxStyle | React.CSSProperties | Body cell card / frame (background, radius, borders, shadow) | | valueStyle | React.CSSProperties | Body cell content (color, font weight, etc.); frame keys belong in boxStyle | | formatOptions | { decimals?, currencySymbol?, dateFormat?, datetimeFormat?, timeFormat?, maxPrimaryChips? } | Number, currency, date, and url formatting (url / fieldType: 'url': inline chips before +N) | | filterType | 'text' \| 'select' \| 'multiselect' \| 'checkbox' \| 'boolean' \| 'date' \| 'number' \| 'dropdown' \| 'radio' | Filter UI type | | filterOptions | { label: string; value: any }[] | Options for select, multiselect, dropdown, and radio filters | | actions | TableAction[] | Row action buttons (Add, View, Edit, Delete, List, or custom) | | invokeActionClickCallback | boolean | Actions only — false opts into package navigation when an action includes navigationUrl/navigate (unless overridden per action) | | checkboxSelector | boolean | When true, column becomes the row selection checkbox selector (header select-all + per-row checkbox) | | fieldNavigationInfo | TableFieldNavigationInfo | Clickable/hoverable cell: built-in navigation (router URL update) or dialog (see below) | | aggregationFn | AggregationFnOption<any> | Enables group footer aggregates for this column (under expanded groups) | | groupFooter | SabFooterPresentation | Presentation for group footer cells (label + styling such as Avg: / Total:) | | footerAggregationFn | AggregationFnOption<any> | Enables list footer (<tfoot>) aggregates for this column | | footer | SabFooterPresentation | Presentation for list footer cells | | footerScope | 'filtered' \| 'page' | List footer aggregate scope: filtered totals across all filtered rows, page totals current page only |

Filter placeholders

  • Text, number, and dropdown/select filters: search placeholder (🔎︎).
  • Date, datetime, and time filters: calendar icon (SVG) when empty, plus optional text placeholder; Flatpickr for picking date/time.
  • Rich text (fieldType: 'richtext' or legacy dataType: 'richtext'): the filter row does not render a filter control for that column (HTML is not meant to be filtered as raw markup).
  • Operand persistence (text / number filters): when the user clears the filter value (empty input or clear control), all rows match again (advancedFilterFn treats empty compare value as “no filter”), but the grid keeps { value: '', operand: <user’s choice> } in column filter state so the operand selector does not snap back to the column default until the user picks a different operand.

Currency (fieldType: 'currency')

Use meta: { fieldType: 'currency' } (or legacy dataType: 'currency') for monetary values. This is the grid’s currency formatter (analogous to a template “currency pipe”): it formats numbers for display and export, it does not perform FX conversion.

formatOptions:

| Option | Default | Description | |--------|---------|-------------| | currencySymbol | '$' | Prefix shown before the numeric part (UI and plain-text export). | | decimals | 2 | Minimum and maximum fractional digits (toLocaleString). |

Cell rendering: DataFormatter shows the symbol and a locale-grouped amount (bold weight in the default theme). Null, undefined, or non-numeric values render as symbol + zero with the configured decimal count.

Export / copy: formatCellValueAsPlainText uses the same Intl or legacy rules so CSV/Excel and clipboard text match on-screen presentation.

Aggregations: Currency columns work with meta.aggregationFn / meta.footerAggregationFn like other numeric kinds (for example sum) when the underlying cell values are numbers.

Rich text (fieldType: 'richtext')

  • Cell UI: shows a View Detail control; body uses sanitized HTML (RichTextFormatter).
  • Header: the package exports RichtextHeaderIcon so host apps or JSON-driven configs can show a consistent RICHTEXT marker beside the title (optional glyph override).
  • Filter row: no filter panel for this column (see above).
  • Export / plain text: formatCellValueAsPlainText strips tags, removes media elements where possible, and collapses whitespace so spreadsheets get readable text (aligned with what users read from the detail view, without HTML noise).
  • Grouping: URL and rich-text kinds cannot be used as group keys; the grid sets enableGrouping: false for those columns so drag-to-group stays meaningful.

URL field type (fieldType: 'url')

Use meta: { fieldType: 'url' } (or legacy dataType: 'url') so the grid treats the column as structured links, not generic text.

Cell value shapes (normalized in code):

| Shape | Behavior | |-------|----------| | Single object | { urlValue } or { displayValue?, urlValue } — one chip. If displayValue is omitted or empty, the chip label is the URL. | | Array | Mix of objects { displayValue?, urlValue }, { urlValue }, or plain strings — each becomes one link. | | Plain string | Treated as URL; optional https:// is prepended for values like www.example.com when there is no scheme. |

Display vs hover:

  • Chip shows display when provided; otherwise the URL text.
  • Hover (title on chip and anchor): if a non-empty display exists and differs from the URL → display:url (no space after the colon). If only a URL is shown → tooltip is only the URL.

Multiple URLs in one cell:

  • Up to formatOptions.maxPrimaryChips chips inline (default capped at 2 in the formatter for list UX). Additional entries appear under a +N control; the overflow list opens in a fixed portal so table overflow does not clip it.

Filtering:

  • The advanced filter compares against urlValue only (objects/arrays are flattened to URL strings), so users can type https://… or a path fragment without matching display labels.

Export:

  • CSV/Excel export uses formatCellValueAsPlainText: for url, exported text is comma-separated urlValue list only (no JSON, no display labels).

Grouping: fieldType: 'url' is not a valid group key; enableGrouping is forced off for that column (same as rich text).


Actions (from config / JSON)

Actions are fully driven by column meta.actions. Each action can specify:

| Field | Type | Description | |-------|------|-------------| | id | string | Unique key passed to onActionClick | | label | string | Button label (optional for icon-only buttons) | | icon | 'View' \| 'Edit' \| 'Delete' \| 'Add' \| 'List' or lowercase in JSON, or ReactNode | Built-in icon or custom node | | navigate | string \| (row) => string | URL or function; use in onActionClick for routing (e.g. router.push(...)) | | navigationUrl | string \| (row) => string | Preferred alias of navigate (used by built-in action navigation) | | actionNavigationBehavior | 'builtin' \| 'callback' | callback (default): your onActionClick / action.onClick runs. builtin: grid performs built-in router/dialog navigation (same as field navigation). | | navigationType | 'router' \| 'dialog' | Built-in action navigation type (defaults to router) | | preserveLocation | boolean | Built-in router only — keep current URL and store context in location.state (see Field navigation) | | builtinDialogPreset | 'embedCurrentRoute' \| 'fieldContextCard' | Built-in dialog only — package-provided dialog bodies | | onClick | (row) => void | Per-action handler (if not using table onActionClick) | | show | (row) => boolean | Hide button for specific rows | | className | string | Extra CSS class for the button | | buttonStyle | React.CSSProperties | Inline styles on the action <button> | | labelStyle | React.CSSProperties | Inline styles on the label <span> (icon / label modes) |

Built-in icon keys: View, Edit, Delete, Add, List (exported as ACTION_ICON_KEYS). In JSON you can use lowercase ("view", "add", etc.).

Example:

meta: {
  actions: [
    { id: 'add', label: 'Add', icon: 'Add', navigate: '/create' },
    { id: 'view', label: 'View', icon: 'View', navigate: (row) => `/item/${row.id}` },
    { id: 'edit', label: 'Edit', icon: 'Edit', navigate: (row) => `/edit/${row.id}` },
    { id: 'list', label: 'List', icon: 'List', navigate: '/list' },
  ],
}

react-router-dom (built-in navigation)

When you use meta.invokeActionClickCallback: false (package resolves action.navigate) or built-in field navigation with type: 'router', the grid calls react-router’s navigate() for you.

Default: render SabReactTable inside your app’s <Router> / <Routes> tree (add react-router-dom; it is an optional peer). The table detects the router context and wires navigation internally — no routerNavigate prop is required.

Override: pass routerNavigate yourself if you use a custom history, tests, or a table instance that is not under a Router:

import { useNavigate } from 'react-router-dom'

function MyGrid() {
  const navigate = useNavigate()
  return (
    <SabReactTable
      data={rows}
      columns={columns}
      routerNavigate={(to, o) => navigate(to, { replace: o?.replace, state: o?.state })}
    />
  )
}

http(s):// URLs still use a full document navigation (location.assign). Dialog / popup modes are unchanged.

Linked package / monorepo (Vite): if the table is already under BrowserRouter but built-in navigation still warns, the bundler may be resolving two copies of react-router-dom, so the grid’s hooks do not see your router context. Add resolve.dedupe for react-router and react-router-dom (see sab-grid-demo vite.config.ts). Avoid aliasing react-router to a single file — that breaks subpath imports such as react-router/dom.

Optional helpers: getActionClickChannel, type SabInteractionChannel — see column meta.invokeActionClickCallback.


Field navigation (click / hover)

Use column meta.fieldNavigationInfo to mark data cells (default formatter or date cells) as navigable. This is separate from meta.actions. Custom column.cell renderers are not wrapped; action columns take precedence.

Click vs hover (timing, keyboard, data-sab-* attributes, and minimal samples) is documented under Field navigation: click vs hover (see the Features section above).

Builtin vs callback: fieldNavigationBehavior controls whether the grid performs navigation (builtin, default) or calls your app (callback) via onFieldNavigation.

| Field | Type | Description | |-------|------|-------------| | id | string | Stable identifier (telemetry, tests, data-sab-field-id) | | triggerEvent | 'click' \| 'hover' | click: immediate on click + Enter/Space when focused. hover: ~400ms dwell after mouseenter (cleared on mouseleave) before firing | | type | 'router' \| 'dialog' | router: in-app URL (automatic navigate() when under Router). dialog: callbackonFieldNavigation; builtin → grid modal + renderFieldNavigationDialog (not onFieldNavigation) | | navigationUrl | string \| (row) => string | In-app path for router; URL hint for dialog/iframe; normalized for router when needed | | navigate | string \| (row) => string | Deprecated alias of navigationUrl | | fieldNavigationBehavior | 'builtin' \| 'callback' | builtin (default): grid navigates or opens dialog. callback: grid calls onFieldNavigation only | | preserveLocation | boolean | Builtin router only — keep current URL and pass context via location.state only | | builtinDialogPreset | 'embedCurrentRoute' \| 'fieldContextCard' | Builtin dialog only — package-provided dialog bodies | | show | (row) => boolean | Optional row filter |

Builtin router context: The grid passes SAB_FIELD_NAV_CONTEXT_KEYS inside location.state[SAB_FIELD_NAV_ROUTER_STATE_KEY] (not in the URL query string). Read it on the destination route with useLocation().

UI: Interaction hint on the column header. Cells expose data-sab-field-interaction, data-sab-field-id, data-sab-column-id.

Router wiring: Usually omitted — the table uses react-router’s navigate() when rendered under a Router. Pass routerNavigate only to override.

// Builtin router: path only; context in location.state (see SAB_FIELD_NAV_ROUTER_STATE_KEY)
meta: {
  fieldNavigationInfo: {
    id: 'open-region',
    triggerEvent: 'click',
    type: 'router',
    navigationUrl: () => '/regions/detail',
    // preserveLocation: true, // keep URL unchanged; context flows via location.state
  },
},
// Callback dialog: app modal via `onFieldNavigation` (recommended for dialogs)
meta: {
  fieldNavigationInfo: {
    id: 'units-preview',
    triggerEvent: 'hover',
    type: 'dialog',
    fieldNavigationBehavior: 'callback',
  },
},
// Full app control: no grid navigation
meta: {
  fieldNavigationInfo: {
    id: 'custom',
    triggerEvent: 'click',
    type: 'router',
    fieldNavigationBehavior: 'callback',
    navigationUrl: (row) => `/items/${(row as { id: string }).id}`,
  },
},

Aggregations (group footer + list footer)

Aggregations are opt-in per column and rendered as:

  • Group footer rows (under expanded groups): enable with column meta.aggregationFn (or TanStack column.aggregationFn), and optionally style with meta.groupFooter.
  • List footer row (<tfoot>): enable with column meta.footerAggregationFn, optionally style with meta.footer, and control scope with meta.footerScope ('filtered' vs 'page').

Example:

import type { ColumnDef } from '@tanstack/react-table'

type Row = { id: string; amount: number; region: string }

const columns: ColumnDef<Row, unknown>[] = [
  { id: 'region', accessorKey: 'region', header: 'Region' },
  {
    id: 'amount',
    accessorKey: 'amount',
    header: 'Amount',
    meta: {
      dataType: 'currency',
      aggregationFn: 'sum',
      groupFooter: { label: 'Total:', labelClassName: 'font-semibold' },
      footerAggregationFn: 'sum',
      footerScope: 'filtered',
      footer: { label: 'Total:', labelClassName: 'font-semibold' },
    },
  },
]

Pivot view footers are configured separately in pivotConfigJSON.footerTotals (not via list meta.footerAggregationFn). Only number, decimal, and currency row-value fields are eligible. See Matrix / pivot comparison below.


Matrix / pivot comparison

Enable one setup mode per feature (predefined JSON or interactive picker — not both). Matrix and pivot are independent: you may combine predefined matrix with interactive pivot on the same grid.

| Prop | Mode | |------|------| | matrixConfigJSON | Predefined Matrix (toolbar toggle) | | enableInteractiveMatrix | Interactive Matrix (context menu → COMPARISON) | | pivotConfigJSON | Predefined Pivot (toolbar toggle) | | enableInteractivePivot | Interactive Pivot (context menu → COMPARISON) |

Users must select at least 2 rows in list view before matrix/pivot comparison runs. The grid auto-injects a checkbox column for row selection — no host checkbox column or getRowId prop is required unless you opt out or your rows lack an inferrable unique key (see Row selection).

<SabReactTable
  data={rows}
  columns={columns}
  pivotConfigJSON={{
    rowTitle: 'region',
    columnTitle: 'fiscalQuarter',
    rowValues: ['revenueAmount', 'unitsSold'],
    footerTotals: [
      { field: 'revenueAmount', aggregationFn: 'sum', label: 'Total:' },
      { field: 'unitsSold', aggregationFn: 'sum' },
    ],
  }}
/>

Matrix (transpose layout) — unified JSON shape { columnTitle, rowValues } shows each field as a row and each selected record as a column.

PivotrowTitle × columnTitle cross-tab; rowValues are numeric measures only.

  • Pivot footers use footerTotals in pivotConfigJSON (or Interactive Pivot Pivot Totals picks: sum, mean, min, max only). List footer settings are not applied in pivot view.
  • In matrix/pivot view: the row-axis column has an empty header, is excluded from the Columns menu, and is auto-frozen on horizontal scroll; column resize is enabled.

Conflicting props (e.g. pivotConfigJSON + enableInteractivePivot) are caught at compile time via SabMatrixPivotHostPropsExclusive.

Headless API: useMatrixPivotViewState, useMatrixTransform, usePivotTransform, normalizeMatrixConfigJSON, normalizePivotConfigJSON, MatrixGrid, PivotGrid — see §21 Matrix & Pivot Comparison in the technical reference below.


Row selection (checkbox selector column)

Row selection is enabled when you add a checkbox selector column:

  • Preferred: set column meta.checkboxSelector: true
  • Legacy: a column id ending in '_checkbox_selector' is treated as a selector

When enabled, the grid renders a header select-all checkbox and per-row checkboxes, and calls onRowSelectionChange(selection, selectedRows).


Example: filters and formatting

const columns: ColumnDef<Person, unknown>[] = [
  { id: 'id', accessorKey: 'id', header: 'ID', meta: { dataType: 'number' } },
  { id: 'name', accessorKey: 'name', header: 'Name', meta: { dataType: 'text' } },
  { id: 'email', accessorKey: 'email', header: 'Email', meta: { dataType: 'email' } },
  {
    id: 'role',
    accessorKey: 'role',
    header: 'Role',
    meta: {
      dataType: 'text',
      filterType: 'select',
      filterOptions: [
        { label: 'Admin', value: 'Admin' },
        { label: 'User', value: 'User' },
      ],
    },
  },
]

Export

  • Excel: exportTableToExcel(data, filename?, tableTitle?) – uses ExcelJS; suitable for larger files. NPM audit issues from transitive deps are addressed via overrides in package.json (minimatch).
  • CSV: exportTableToCSV(data, filename?) – plain string/Blob; no ExcelJS.

Both are available from the toolbar Data Export menu and as standalone imports.


Exports

| Export | Description | |--------|-------------| | SabReactTable | Main grid component | | NormalGrid, NormalGridProps | Alias for SabReactTable / SabReactTableProps | | SabReactTableProps, LayoutMode, SabGridTheme, SabGridResolvedTheme, SabPageChangeInfo, OnActionClickHandler, OnFieldNavigationOverrideHandler | Component, layout, theme, and pagination types | | SabGridColumnDef | ColumnDef with typed meta: SabColumnMeta | | SabColumnMeta, SabIntlCurrencyFormatOptions | Column meta union and Intl currency options | | RichtextHeaderIcon, RichtextHeaderIconProps | Optional column header glyph for fieldType: 'richtext' | | DataFormatter, ActionFormatter | Default cell and action formatters | | formatCellValueAsPlainText, formatCurrencyIntlValue, getSabIntlCurrencyNumberFormat, normalizeRichtextHtmlValue | Export / clipboard plain text; Intl currency; HTML normalization | | RichTextFormatter, RichTextFormatterProps | Sanitized HTML body used inside the rich-text detail view | | formatDateValue, renderDateCell, isIsoDateLike | Date formatting helpers | | ActionIcons, ACTION_ICON_KEYS, TableAction, ActionConfig, ActionIconKey, ActionDisplayMode | Action types and built-in icons | | FieldNavigationCellWrapper, resolveFieldNavigate, buildFieldNavigationPayload, buildBuiltinRouterNavigateOptions, buildInternalFieldNavigationContext, isFieldNavigationMetaPlausible, normalizeNavigationPath, SAB_FIELD_NAV_CONTEXT_KEYS, SAB_FIELD_NAV_ROUTER_STATE_KEY, TableFieldNavigationInfo, FieldNavigationType, FieldNavigationTriggerEvent, FieldNavigationPayload | Field navigation config and helpers | | FieldNavigationDialogRenderArgs, SabRouterNavigateFn | Router/dialog integration types | | exportTableToExcel, exportTableToCSV, buildExportFooterRowFromTable, SabExportFooterOptions | Standalone export helpers | | resizeColumnsByCellContent, getWidthInPixel, getWidthInPixelTitle, applyFreezeSplitFitContentSizing | Column resize utilities | | ColumnResizeDef, ResizeColumnsByCellContentParams | Resize utility types | | DateFormatOptions | Date format options type | | ColumnDef, RowSelectionState | Re-exported from @tanstack/react-table | | isCheckboxSelectorColumn, evaluateFreezeUpto, FREEZE_TOAST_* | Row selection column helper, freeze validation | | sabAggregationFns, applySabColumnAggregationDefaults, buildDisplayRowsWithGroupFooters, … | Group/list footer aggregation utilities | | resolveColumnFieldKind, getColumnFieldKind | fieldType / dataType resolution | | SabGridSidePanel, SabGridSidePanelProps | Right-docked slide panel shell (advanced embedding) | | FacetPanel, FacetChip, FacetHorizontalBar, FacetInlineGroup, FacetTooltip | Facet UI building blocks | | ColumnFacetSearchSidePanelBody, SabFacetFilterMenuIcon | Column side-panel facet search UI | | applyFacetFilters, buildFacetUiWithCounts, calculateFacetCounts, computeFacetCountsByField, isHostSuppliedServerPage, getClientFilteredData, hasActiveFacetFilters, buildFacetConfiguration | Facet filter/count utilities | | resolveFetchModeBodyRows, getDataBackedFetchTotal, mergeServerFacetCountsIntoConfiguration | Deprecated fetch-mode helpers (backward compatibility) | | FacetConfigEntry, FacetClickPayload, FacetClickResult, FacetOption, ActiveFacetFilters, SelectedFacet, FacetTableConfig, DEFAULT_FACET_STYLE | Facet configuration and callback types | | useMatrixPivotViewState, useMatrixTransform, usePivotTransform | Matrix/pivot view state and memoised transform hooks | | transformDatasetToMatrixRows, transformDatasetToPivotRows, buildMatrixColumnDefs, buildPivotColumnDefs | Headless matrix/pivot row + column builders | | normalizeMatrixConfigJSON, normalizePivotConfigJSON, resolveMatrixPivotHostOptions | JSON normalisation and host prop resolution | | MatrixGrid, PivotGrid, MatrixPivotCustomConfigPanel | Comparison grid wrappers and interactive config panel | | SabMatrixPivotHostPropsExclusive, SabMatrixConfigJSON, SabPivotConfigJSON, SabPivotFooterFieldConfig, SabMatrixPivotViewMode | Matrix/pivot host and config types |

Subpaths: @chainsys/sab-react-grid/tailwind-content — CommonJS module exporting a string[] of glob paths for Tailwind content (see Tailwind CSS and theming).


Technical documentation

The reference below is injected from TechnicalDocumentation.md so it appears on npm with the same Markdown rendering as the rest of this readme.

Table of Contents

  1. Overview
  2. Repository Structure
  3. Architecture
  4. SabReactTable — Main Component
  5. Column Meta — Extended Type System
  6. URL Field Type — Custom Formatter
  7. RichText Field Type — Custom Formatter
  8. Field Navigation
  9. Action Buttons
  10. Filtering
  11. Aggregations
  12. Export (Excel & CSV)
  13. Column Resize Utilities
  14. Theming
  15. Public API — Full Exports Index
  16. Usage Examples
  17. Security
  18. Dependencies
  19. Changelog Summary
  20. Facet Search
  21. Matrix & Pivot Comparison

1. Overview

@chainsys/sab-react-grid is an enterprise-grade, TanStack Table–powered React data-grid library developed by Chainsys. It ships as a dual-format (ESM + CommonJS) npm package with full TypeScript declarations and is designed to be dropped into any React 18/19 application that uses Tailwind CSS.

Package identity:

| Attribute | Value | |---|---| | NPM scope & name | @chainsys/sab-react-grid | | Current version | 2.2.0 | | License | MIT | | Node requirement | >=16 | | React peer | ^18.0.0 \|\| ^19.0.0 | | TanStack Table peer | 8.21.3 | | TanStack Virtual peer | 3.13.23 | | Tailwind CSS peer | ^4.2.2 (optional) |

Key capabilities at a glance: row virtualisation, multi-column sorting, per-column filtering (text/number/date/select/boolean), facet search (client filter + server fetch), row grouping with aggregations, column freeze/split-freeze, column drag-reorder, column resize, Excel (.xlsx) + CSV export, light/dark theme, field navigation (click/hover → router or dialog), URL chip cells, RichText cells, action buttons, matrix / pivot comparison views (predefined JSON or interactive field pickers), and pagination callbacks (onPageChange, onItemsPerPageChange).


2. Repository Structure

The project root contains the following key files and directories:

| Path | Purpose | |---|---| | src/ | All TypeScript source files (compiled to dist/) | | src/SabReactTable.tsx | Main component — SabReactTable + all internal UI | | src/index.ts | Public package entry — re-exports all public API symbols | | src/SabBuiltinFieldDialogPresetBodies.tsx | Preset dialog bodies (embedCurrentRoute, fieldContextCard) | | src/components/RichtextHeaderIcon.tsx | SVG header badge for richtext columns | | src/components/grid/NormalGrid.tsx | Alias export: NormalGrid = SabReactTable | | src/components/grid/ColumnVisibilityMenu.tsx | Column show/hide dropdown | | src/formatters/RichTextFormatter.tsx | Standalone richtext renderer (DOMPurify + CSS) | | src/utils/tableFormatters.tsx | DataFormatter, ActionFormatter, URL chip, date renderers, types | | src/utils/columnFieldKind.ts | resolveColumnFieldKindfieldType / dataType resolution | | src/utils/sabAggregation.ts | Group + list footer aggregation engine | | src/utils/sabFilterNormalize.ts | Filter operand normalisation helpers | | src/utils/exportUtils.ts | Excel (ExcelJS) + CSV export utilities | | src/utils/fieldNavigationResolve.ts | Payload builders for field / action navigation | | src/utils/richtextDetailOverlay.tsx | Portal modal for richtext detail view | | src/utils/tableColumnResizeUtils.ts | Fit-to-content column width calculation | | src/utils/sabFacetSearch.ts | Facet types, config resolution, row value helpers | | src/utils/facet/ | Facet config builder, count engine, filter engine, client filter | | src/components/grid/facet/ | Facet workspace UI (panel, chips, carousel, horizontal bar) | | src/components/grid/ColumnFacetSearchSidePanel.tsx | Searchable column facet side panel | | src/components/grid/SabGridSidePanel.tsx | Right-docked slide panel shell (facet side panel, shared z-index/portal patterns) | | src/components/grid/ColumnVisibilityMenu.tsx | Column show/hide checklist (internal; not exported from package entry) | | src/matrixPivot/ | Matrix/pivot transforms, column builders, field-meta helpers | | src/hooks/useMatrixPivotViewState.ts | View-mode state for list / matrix / pivot | | src/utils/matrixPivotNormalize.ts | JSON → internal config normalisation | | src/utils/resolveMatrixPivotHostOptions.ts | Host prop resolution and conflict validation | | src/components/grid/ComparisonGrid.tsx | MatrixGrid / PivotGrid comparison wrappers | | src/components/grid/MatrixPivotCustomConfigPanel.tsx | Interactive matrix/pivot field-picker panel | | dist/esm/ | ESM build with .d.ts declarations | | dist/cjs/ | CommonJS build | | tailwind-content.cjs | Tailwind content globs for host tailwind.config | | package.json | Package manifest, scripts, peer deps | | tsconfig.esm.json / tsconfig.cjs.json | TypeScript build configs |


3. Architecture

The library is structured in three conceptual layers that compose to produce the final grid.

3.1 Component Layer

SabReactTable is the single public React component. Internally it is split into a thin router-aware shell (SabReactTableSabReactTableAutoRouterNavigate) and the real implementation in SabReactTableCore. This avoids breaking the Rules of Hooks when useNavigate() / useLocation() are conditionally unavailable (non-Router context).

Internal sub-components (not exported individually):

  • MemoizedHeaderCell — renders one <th> with sort, filter menu, freeze-up-to, drag handles, richtext badge, field-nav glyph.
  • MemoizedFilterCell — renders the per-column filter input row (text, number, date/flatpickr, select, multiselect, boolean, radio, dropdown).
  • ColumnVisibilityMenu — show/hide column panel (exported separately for embedded use).
  • RichtextDetailOverlayProvider + modal — portal dialog that renders sanitised HTML for richtext cells.
  • SabBuiltinFieldDialogEmbedCurrentRoute / SabBuiltinFieldDialogFieldContextCard — preset dialog bodies.

3.2 Utility Layer

Pure functions and hooks with no React component state — all exported from the public index.

3.3 Build System

TypeScript is compiled twice: once for ESM (tsconfig.esm.json, moduleResolution bundler, target esnext) and once for CommonJS (tsconfig.cjs.json). Both emit declaration files (.d.ts + source maps). Tailwind scanning is enabled via the tailwind-content.cjs subpath export so host apps include this package's dist/ in their Tailwind content array.

3.4 Data Flow

Host app passes data[] + columns[]
  → TanStack useReactTable()
  → sorted/filtered/grouped row model
  → @tanstack/react-virtual virtualiser renders only visible rows
  → each row cell calls DataFormatter (or custom cell)
  → on export, formatCellValueAsPlainText strips HTML/objects to plain text

4. SabReactTable — Main Component

File: src/SabReactTable.tsx

The primary export. Accepts a generic TData type for strong typing of row data. The component manages all TanStack table state internally (sorting, filtering, grouping, pagination, column visibility, column order, column sizing, row selection, expansion) and exposes callbacks for events the host needs to act on.

4.1 Props Reference

| Prop | Type | Default | Description | |---|---|---|---| | data | TData[] | (required) | Row dataset. Client facet mode: full body + count source. Fetch mode: pass the current page (pair with totalRowCount) or a buffer of rows — the grid renders data immediately on mount. Facet selection uses {@link onFacetClick}; pagination uses {@link onPageChange} / {@link onItemsPerPageChange}. Supply {@link FacetClickResult.facetCountsByField} when data is a page slice so counts are not derived from the body rows alone. | | columns | ColumnDef<TData>[] | (required) | TanStack column definitions. | | title | string | 'Table_List' | Toolbar title and export filename prefix. | | onSortedDataChange | (rows: TData[]) => void | undefined | Fired when visible/sorted rows change. | | defaultPageSize | number | 50 | Initial page size. | | pageSizeOptions | number[] | [10,20,50,100,500,1000] | Dropdown options for items-per-page. | | onPageChange | (pageInfo: SabPageChangeInfo) => void \| Promise<FacetClickResult> | undefined | Fired after toolbar prev/next. page is 1-based. May return { facetCountsByField } to refresh chip counts without recounting the page slice. Pair with totalRowCount + currentPage in fetch mode for host-controlled paging. | | onItemsPerPageChange | (pageSize: number) => void \| Promise<FacetClickResult> | undefined | Fired after items-per-page dropdown change. Resets to page 1 when totalRowCount + onPageChange are set (host-controlled paging). May return { facetCountsByField }. | | totalRowCount | number | undefined | Server-side pagination total (toolbar page count). | | currentPage | number | undefined | 1-based active page when using totalRowCount. | | facetConfig | FacetConfigInput | undefined | Facet field definitions (array or JSON object). | | onFacetClick | `(pay