Skip to content

Changelog

Latest updates and release notes for Niko Table.

  • Virtualized table — column spacing no longer compresses on wide datasetsDataTableVirtualizedBody’s column-lock useLayoutEffect now sets tableEl.style.minWidth to the sum of all visible column.getSize() values before measuring column widths. Without this, the w-full style on <table> forced the element to fit its scroll container, causing the auto-layout algorithm to distribute compressed widths and lock them in permanently. Setting minWidth lets the table expand beyond the container width so overflow-auto on the container handles horizontal scrolling, while auto-layout distributes space based on actual content. The minWidth is cleared on every unlock/reset cycle so toggling column visibility re-measures correctly. Explicit size values on column defs are respected — they raise the minWidth floor proportionally.
  • All product-based examples enriched with four new columnsbrand (text), rating (1–5 with ★), revenue (price × stock, formatted with toLocaleString), and releaseDate (toLocaleDateString) added to all eight product example files: basic, basic-state, infinite-scroll-table, infinite-scroll-table-state, infinite-scroll-virtualized-table, infinite-scroll-virtualized-table-state, virtualization-table, and virtualization-table-state. The four infinite-scroll files use index-based deterministic generation (no Math.random) so the values are stable across SSR and client hydration. The two virtualization files retain Math.random for variety, consistent with their existing approach. The two state-variant files also gain a Brand DataTableFacetedFilter in the toolbar alongside the existing Status filter.
  • DataTableLoadingMore — Composable “loading more” row for infinite-scroll standard tables. Self-gates on its isFetching prop — renders a spinner + label when true, nothing when false. Drop it as a child of DataTableBody alongside DataTableSkeleton and DataTableEmptyBody.
  • DataTableVirtualizedLoadingMore — Virtualized variant. Sits outside the virtualizer’s row count so it doesn’t affect estimateSize math.
  • onNearEnd + prefetchThreshold on DataTableVirtualizedBody — Virtualizer-index-driven prefetch trigger for infinite scroll. Fires when the last rendered virtual row is within prefetchThreshold rows (default 10) of the end of the dataset. Strictly better than onScrolledBottom for infinite scroll — catches fast scrolls, scrollbar drag, scrollToIndex() jumps, and initial renders where data doesn’t fill the viewport. Called at most once per false→true transition.
  • Virtualized table — native <table> layout with measure-and-lock column sizing — Replaced display: block/flex on virtualized table rows and cells with native table layout. The browser’s table layout algorithm now distributes column widths based on content. DataTableVirtualizedBody uses a useLayoutEffect to measure each <th>’s computed width after first data render, then locks the table to table-layout: fixed so columns stay stable during virtual scroll. The effect runs every render but the fast path (already locked, same column count) is two ref reads — negligible at 60 fps. Column visibility changes (toggle, reorder) trigger a fresh measurement cycle. Spacer rows use <tr><td colSpan={n}> to stay within the table layout context. All child components (DataTableVirtualizedEmptyBody, DataTableVirtualizedSkeleton, DataTableVirtualizedLoading, DataTableVirtualizedLoadingMore) updated to match.
  • Scroll listener — onScrolledBottom / onScrolledTop silently dead — In all four body components (DataTableBody, DataTableVirtualizedBody, and both virtualized DnD bodies), the scroll-listener effect early-returned on !onScroll, which meant onScrolledBottom / onScrolledTop callbacks were never wired unless the consumer also passed onScroll. The listener now attaches whenever any of the three callbacks is provided, and onScroll is invoked conditionally.
  • Infinite Scroll Table — New example page with full implementation guide for non-virtualized infinite scroll (500-row mock, onScrolledBottom + DataTableLoadingMore). Includes a caution callout recommending the virtualized variant for most use cases.
  • Infinite Scroll Virtualized Table — New example page for virtualized infinite scroll (5,000-row mock, onNearEnd + DataTableVirtualizedLoadingMore). Includes comparison table of onNearEnd vs onScrolledBottom, TanStack Query wiring pattern, and controlled-state variant.
  • Core overview — Added DataTableLoadingMore, DataTableVirtualizedLoadingMore, onNearEnd, and prefetchThreshold to the component and props reference.
  • Faceted filter — dynamicCounts silently ignored in key paths — The prop was accepted on DataTableFacetedFilter and useFacetedOptions but the fallback useMemo and the auto-generated branch of useGeneratedOptions both computed counts from the wrong row set. Setting dynamicCounts: false while keeping limitToFilteredRows: true had no effect. Counts now come from countSourceRows in every branch, and the useMemo deps include dynamicCounts so React recomputes when it changes.
  • Faceted filter — explicit options bypassed count enrichment entirely — When a caller passed static options to DataTableFacetedFilter or TableColumnFacetedFilterMenu, both components short-circuited and returned the caller array untouched. dynamicCounts was a no-op on that path, so multi-select filters with static options never showed live counts, and the single-select path only filtered options by presence without populating count. Both entry points now enrich caller-supplied options through the same optionRows / countRows split used for auto-generated options, and honor limitToFilteredRows and showCounts on both the fast and slow paths.
  • Faceted filter — TableColumnFacetedFilterMenu dropped dynamicCounts on the way to the hook — The column menu wired the prop into its fallback path but called useGeneratedOptionsForColumn({ limitToFilteredRows }), silently omitting dynamicCounts. The base hook always saw the default true. It now forwards { limitToFilteredRows, dynamicCounts }, matching the parallel wiring in DataTableFacetedFilter.
  • Faceted filter — zero-count options no longer disappear — Options discovered in the filtered row set were silently dropped when their count in the count row set was 0. Only visible when limitToFilteredRows !== dynamicCounts, but when it surfaced it looked like options were flickering in and out. Fixed by separating option discovery from count computation.
  • Faceted filter — multi-select now defaults limitToFilteredRows to false — Single-select keeps the previous true default. A narrowing multi-select facet produces a broken UX: un-checking the last selected value can also remove other still-selected values from the visible list, because their rows were just filtered away. Expressed as limitToFilteredRows ??= !multiple, so explicit props always win. DataTableFacetedFilter, DataTableFacetedFilterContent, and TableColumnFacetedFilterMenu all apply the default at their component entry points.
  • preserve merge strategy contract clarifiedpreserve returns user-defined meta.options untouched (no count injection), respecting limitToFilteredRows only to hide values not present in the current row set. Only augment injects counts. An earlier refactor draft blurred the distinction; reverted before shipping.
  • First behavioral test suite for the library — Introduced Vitest as the unit/component runner. Added vitest, @vitejs/plugin-react, @testing-library/react, @testing-library/dom, jsdom, and @vitest/ui as dev dependencies. New scripts: pnpm test (run once) and pnpm test:watch.
  • 23 tests across three files — Every (limitToFilteredRows × dynamicCounts) permutation on a real TanStack table, the preserve / augment merge-strategy branches, zero-count options, and prop-wiring regressions for both TableColumnFacetedFilterMenu and DataTableFacetedFilter. Each fix above is pinned by at least one test. Fixture columns declare explicit filterFns so row filtering actually happens — a subtlety worth noting for future contributors.
  • GitHub Actions CI workflow.github/workflows/ci.yml runs pnpm test, eslint, and pnpm build on every push to main and PR targeting main. Tracks Node LTS automatically via node-version: "lts/*". Complements the existing registry.yml post-deploy smoke workflow.
  • Registry example exercises the new defaultfaceted.tsx’s multi-select category filter previously set limitToFilteredRows={false} explicitly; it now relies on the !multiple default, so any regression will surface visually on the docs site’s Faceted Filter Table example page.
  • DataTableFacetedFilterlimitToFilteredRows default now shown as !multiple in the props table with both single-select and multi-select branches described.

  • Row click: event delegation — All table body components (standard, virtualized, and DnD variants) now use a single delegated row-click handler instead of one handler per row or per cell. This reduces allocations and keeps behavior consistent across DataTableBody, DataTableVirtualizedBody, DataTableVirtualizedDndBody, DataTableVirtualizedDndColumnBody, DataTableDndBody, and DataTableDndColumnBody.
  • DataTableRoot — Global filter and default column-pinning handlers are memoized with useCallback so table options stay stable and the table does not re-initialize unnecessarily.
  • TablePagination — Page size, page input, and prev/next button handlers are memoized with useCallback for stable references when pagination state changes.
  • DataTableAside — Trigger toggle and close button handlers are memoized with useCallback.
  • DataTableRoot — Docs now list the required children prop and the optional state prop for controlled mode. The introduction page also documents className.
  • Props tables and examples were reviewed for accuracy against the current implementation.
  • TablePagination — page input draft state — The page number input now uses local draft state so users can type intermediate values (e.g. "1" before completing "12") without the table jumping mid-edit. The page index is only committed on blur or Enter; invalid input resets to the current page.
  • TablePagination — stale page index on page size changeonPageSizeChange now receives the recalculated page index (Math.floor((pageIndex * pageSize) / newPageSize)) instead of the stale pre-change index.
  • Sticky header z-index — Fixed an issue where pinned body cells could overlap the sticky header during scroll. The sticky header container now uses z-30, above header pinned cells (z-20) and body pinned cells (z-10).
  • Pinned header cell top style — Added top: 0 to pinned header cells in getCommonPinningStyles so they stick vertically when the header is sticky.

The first public release of Niko Table — a composable, shadcn-compatible data table component registry built with TanStack Table and React.

  • DataTable - Core data table with sorting, filtering, and pagination
  • DataTableVirtualized - Virtualized rendering for large datasets
  • DataTablePagination - Flexible pagination controls
  • DataTableSearchFilter - Global search across all columns
  • DataTableFacetedFilter - Multi-select faceted filtering with counts
  • DataTableFilterMenu - Advanced filter menu with AND/OR logic
  • DataTableInlineFilter - Inline filter bar with mixed operator support
  • DataTableSliderFilter - Range slider filtering for numeric columns
  • DataTableDateFilter - Date and date range filtering
  • DataTableExportButton - CSV/JSON export
  • DataTableAside - Side panel for row details
  • DataTableSelectionBar - Bulk actions on selected rows
  • DataTableColumnSort - Sort menu and sort icon components
  • DataTableColumnHide - Column visibility toggle
  • DataTableColumnPin - Column pinning (left/right)
  • Row DnD - Drag and drop row reordering via @dnd-kit
  • Column DnD - Drag and drop column reordering
  • Row Selection - Single and multi-row selection with bulk actions
  • Row Expansion - Expandable rows with custom content
  • Tree Table - Hierarchical data with expand/collapse
  • Virtualization - Smooth scrolling for 10k+ rows
  • URL State - Sync filters, sorting, and pagination to URL via nuqs
  • Server-Side - Server-side filtering, sorting, and pagination support
  • 15+ filter operators (contains, equals, greater than, between, etc.)
  • Mixed AND/OR logic with mathematical precedence
  • Per-filter join operator control
  • Regex-cached filter execution for large datasets