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

@vstn-tech/data-table

v0.23.0

Published

React data grid built on TanStack Table + Mantine v9. Clean-room implementation by VSTN Tech.

Readme

@vstn-tech/data-table

A React 19 data grid built on TanStack Table v8, TanStack Virtual, Mantine v9, and dnd-kit. Clean-room implementation owned by VSTN Tech — zero derivation from upstream simple-table.

For AI agents (Claude Code, Cursor, etc.): the canonical usage manual is AGENTS.md — it is self-contained, covers every prop / type / recipe, and is intentionally formatted so you can paste it into context.

Why this library

  • Excel-like UX out of the box: one unified filter popover per column, cell-level selection with Shift/Ctrl-click range extension, arrow-key nav, Ctrl+C copy as TSV, F2/double-click to edit, Tab/Enter to navigate-and-commit.
  • TanStack-native: filter state lives in columnFilters, not a parallel store — you can read and write filters through the standard TanStack API.
  • Fast at 100k rows: row virtualization with variable-height measurement; cell selection store uses useSyncExternalStore so a click re-renders 1-2 cells, not the whole viewport.
  • Mantine themed: inherits theme via CSS variables from your <MantineProvider> — no separate theme prop, no color overrides.
  • Headless or batteries-included: useDataTable() for full headless control, or pass the table instance to <DataTable> for the default shell.

Install

pnpm add @vstn-tech/data-table

Peer dependencies your app must provide:

react             >=19
react-dom         >=19
@mantine/core     >=9
@mantine/hooks    >=9
@tanstack/react-table   >=8.20
@tanstack/react-virtual >=3.10

Optional (depending on features you use):

@mantine/dates    >=9   # required for date filters / date cell editor
dayjs             >=1.11
@dnd-kit/core     >=6   # required for column reorder
@dnd-kit/sortable >=8
@dnd-kit/utilities >=3.2

Wrap your app once:

import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";          // only if you use date features
import "@vstn-tech/data-table/styles.css";

import { MantineProvider } from "@mantine/core";

export function App() {
  return (
    <MantineProvider>
      {/* your app */}
    </MantineProvider>
  );
}

GitHub Packages registry (for the published package):

# .npmrc
@vstn-tech:registry=https://npm.pkg.github.com

Quick start

import {
  DataTable,
  createColumnHelper,
  useDataTable,
} from "@vstn-tech/data-table";

interface Employee {
  id: number;
  name: string;
  role: string;
  salary: number;
  joinedAt: string;
}

const columnHelper = createColumnHelper<Employee>();

const columns = [
  columnHelper.accessor("id", {
    header: "ID",
    size: 80,
    meta: { filterType: "number", align: "right" },
  }),
  columnHelper.accessor("name", {
    header: "Name",
    size: 240,
    meta: { filterType: "text", isEditable: true, editor: "text" },
  }),
  columnHelper.accessor("role", {
    header: "Role",
    size: 180,
    meta: { filterType: "set" },
  }),
  columnHelper.accessor("salary", {
    header: "Salary",
    size: 120,
    meta: { filterType: "number", align: "right" },
  }),
  columnHelper.accessor("joinedAt", {
    header: "Joined",
    size: 140,
    meta: { filterType: "date" },
  }),
];

export function EmployeesPage({ data }: { data: Employee[] }) {
  const table = useDataTable<Employee>({
    data,
    columns,
    initialState: { sorting: [{ id: "salary", desc: true }] },
  });

  return (
    <DataTable
      table={table}
      onRowClick={(emp) => console.log("clicked", emp)}
      onCellEdit={({ rowId, columnId, newValue }) => {
        console.log("commit", rowId, columnId, newValue);
      }}
      style={{ height: 600 }}
    />
  );
}

That's the entire integration. No drawer wiring, no per-page sortedRows useMemo, no parallel filter store.

Column meta

The TanStack ColumnDef is augmented with these meta fields:

| Field | Purpose | |---|---| | filterType | "text" \| "number" \| "date" \| "boolean" \| "set" — drives the filter dropdown UI. Omit to disable filtering for the column. | | filterOptions | Predefined set-filter options. If omitted, options are computed from faceted unique values. | | filterPlaceholder | Placeholder text for the filter input. | | align | "left" \| "right" \| "center" — header + cell alignment. | | ellipsis | Truncate body cell text with . | | pinned | "left" \| "right" — initial column pinning (CSS sticky). | | isEditable | When true, F2 / double-click opens the inline editor. | | editor | "text" \| "number" \| "boolean" \| "date" \| "enum" — which Mantine input to use. | | isInternal | Hides the column from column-editor UIs (selection checkbox, action kebabs). |

Recipes

Filter UI

Each column with meta.filterType shows a icon in its header on hover. Click it to open a single Mantine <Popover> containing:

  • Sort A → Z / Z → A / Clear sort
  • Reset width / Hide column
  • Filter by condition — type-specific inputs:
    • Text: operator (contains / equals / startsWith / …) + value, optional second condition joined by AND/OR
    • Number: comparator (= / > / between / …) + value(s)
    • Date: explicit dates + relative ranges (today / last 7 days / this quarter / YTD / …)
    • Boolean: True / False / Blank
    • Set: faceted checklist with search and (Blanks) row, virtualized for large lists
  • Clear filter (when active)

Filters apply live (text inputs debounce 200ms). All filter state lives in TanStack's columnFilters — read or write it imperatively:

table.setColumnFilters([
  { id: "name", value: { type: "text", primary: { operator: "contains", value: "ada" } } },
  { id: "role", value: { type: "set", values: ["engineer", "manager"] } },
]);

table.getColumn("salary")?.setFilterValue({
  type: "number",
  primary: { operator: "between", value: 80000, value2: 150000 },
});

Excel-like cell selection + clipboard

Enabled by default (enableCellSelection defaults to true):

  • Click a cell → selects + activates
  • Shift+click → extend rectangle from anchor
  • Ctrl/Cmd+click → toggle individual cell
  • Arrow keys → move active (Home / End / Ctrl+Home / Ctrl+End jump to edges)
  • Shift+arrow → extend selection
  • Ctrl+A → select all visible cells
  • Ctrl+C → copy as TSV (paste lossless into Excel / Sheets)
  • Esc → clear

The selection store is fine-grained: each cell subscribes only to its own selection state via useSyncExternalStore, so clicking one cell on a 100k-row table re-renders 1-2 cells, not the whole viewport.

Inline cell editing

Set meta.isEditable: true and meta.editor on a column, then handle the commit:

const columns = [
  columnHelper.accessor("name", {
    header: "Name",
    meta: { isEditable: true, editor: "text" },
  }),
  columnHelper.accessor("status", {
    header: "Status",
    meta: { isEditable: true, editor: "enum", filterOptions: ["draft", "active", "archived"] },
  }),
];

<DataTable
  table={table}
  onCellEdit={({ rowId, columnId, oldValue, newValue, row }) => {
    // Update your data however you like (mutate state, fire a mutation, etc.).
    updateEmployee(rowId, { [columnId]: newValue });
  }}
/>
  • Double-click or press F2 to enter edit mode
  • Enter commits and moves down (Shift+Enter → up)
  • Tab commits and moves right (Shift+Tab → left)
  • Esc cancels
  • Blur outside the cell commits

Column pinning

Set meta.pinned: "left" | "right" for initial pinning; or call table.setColumnPinning(...) programmatically. Renders via CSS sticky positioning with a 1px edge shadow.

Column resize

On by default. Drag the right edge of any header to resize live (uses TanStack's columnResizeMode: "onChange"). Double-click the handle to reset.

Column reorder

Opt in by passing enableColumnReordering. A grip handle appears in each header on hover; drag a header to a new position.

<DataTable table={table} enableColumnReordering />

Grouping + expansion

Pass state.grouping via initialState:

const table = useDataTable({
  data,
  columns,
  initialState: { grouping: ["team", "status"] },
});

Group cells get a ▶ / ▼ expand toggle and (N) count. Nested rows indent by depth.

Pagination

Show the Mantine pagination footer:

<DataTable
  table={table}
  showPagination
  pageSizeOptions={[25, 50, 100, 250]}
/>

Infinite scroll

Provide onLoadMore; the table fires it when the scroll reaches within infiniteScrollThreshold px of the bottom:

<DataTable
  table={table}
  onLoadMore={() => fetchNextPage()}
  infiniteScrollThreshold={300}
  loading={isFetchingNextPage}
/>

Server-side mode

Switch into manual mode on the hook and feed pre-processed rows:

const table = useDataTable<Employee>({
  data: pageData.items,
  columns,
  manualSorting: true,
  manualFiltering: true,
  manualPagination: true,
  pageCount: pageData.totalPages,
  onSortingChange: (updater) => {
    const next = typeof updater === "function" ? updater(sorting) : updater;
    setSorting(next);
    refetch(next, filters, page);
  },
  onColumnFiltersChange: (updater) => {
    const next = typeof updater === "function" ? updater(filters) : updater;
    setFilters(next);
    refetch(sorting, next, page);
  },
  onPaginationChange: (updater) => {
    const next = typeof updater === "function" ? updater(page) : updater;
    setPage(next);
    refetch(sorting, filters, next);
  },
});

CSV export

import { exportToCSV } from "@vstn-tech/data-table";

<Button onClick={() => exportToCSV(table, { fileName: "employees.csv" })}>
  Export CSV
</Button>

CSV respects the current sort + filter + column visibility. Uses UTF-8 with BOM (Excel-compatible). Synchronous; suitable up to ~1M rows.

Loading / error / empty states

<DataTable
  table={table}
  loading={isFetching}
  error={fetchError ? { message: fetchError.message, onRetry: refetch } : null}
  emptyState={<MyCustomEmpty />}
/>

Theming

The table reads colors from Mantine CSS variables — there is no theme prop. Switch your MantineProvider's colorScheme or primaryColor and the table follows.

CSS variables you can override on .dt-root:

.dt-root {
  --dt-row-pad-y: 10px;
  --dt-row-pad-x: 12px;
  --dt-header-h: 36px;
}

For compact density, pass density="compact" on <DataTable>--dt-row-pad-y drops to 4px and --dt-header-h to 28px.

Migrating from ListTableV2

Most props map directly:

| ListTableV2 prop | data-table equivalent | |---|---| | state (from useListTableStateV2) | table (from useDataTable) | | data | (already on useDataTable) | | columns (with meta.filterType) | (same shape; meta is identical) | | loading | loading | | onRowClick | onRowClick | | rowActions | (build your own column with a kebab cell renderer) | | emptyState | emptyState | | idAccessor | getRowId on useDataTable | | pinnedFirstColumn | (use meta.pinned: "left" on the column) | | applyFiltersToData={false} | manualFiltering: true on useDataTable | | page / pageSize / totalRecords | manualPagination: true + pageCount + state.pagination on the hook; showPagination on the component | | enableRowSelection | enableRowSelection on the hook | | selectionToolbar | (render conditionally based on table.getSelectedRowModel()) |

The most substantial behavioral change: filter state now lives in table.getState().columnFilters and writes through column.setFilterValue / table.setColumnFilters — not a parallel state.setFilters. The "no-op setFilterValue" wart from V2 is fixed.

Public API

The library exports:

  • Components: DataTable, DataTablePagination, ActiveFiltersBar, EmptyState, LoadingOverlay, ErrorState, CellEditor, ColumnDropdown, ResizeHandle, DragHandle, SortableHeader
  • Hooks: useDataTable, useIsCellSelected, useIsCellActive, useCellSelectionContext, useEditingContext, useInfiniteScroll
  • Helpers: createColumnHelper, createCellSelectionStore, getPinningStyles, applyFilter, isFilterActive, dataTableFilterFn, computeFacets, buildSelectionTSV, copySelectionToClipboard, buildCSVString, exportToCSV
  • Types: DataTableProps, DataTableColumn, DataTableColumnMeta, DataTableInstance, DataTableState, UseDataTableOptions, DataTableAPI, RowAction, Density, CellEditor (variant), CellAlignment, CellEditCallbackProps, FilterValue, FilterType, TextFilter, NumberFilter, DateFilter, BooleanFilter, SetFilter, and all operator/condition types
  • Constants: CellSelectionContext, EditingContext, EditingProvider, OnCellEditContext

Known limitations / non-goals

  • XLSX export is not yet bundled (CSV only). The TanStack column meta supports the same shape ListTableV2 used (meta.excel); a worker-based XLSX exporter is on the roadmap behind import "@vstn-tech/data-table/xlsx".
  • Sticky parent rows during scroll (group label pinning) — planned for a follow-up minor release.
  • Column editor side-panel (Drawer) — currently consumers build their own using table.setColumnVisibility, table.setColumnOrder, table.setColumnPinning. A pre-built <ColumnEditorDrawer> is on the roadmap.
  • Persistence to localStorage under tableKey — planned; current API accepts tableKey but does not yet wire storage.

Accessibility

  • aria-rowcount on the table; aria-rowindex on each rendered virtual row.
  • Active cell receives tabIndex={0} + focus; non-active cells are removed from the tab order.
  • Sort buttons announce direction via aria-sort-equivalent visible indicators.
  • Filter popovers are Mantine-themed and follow Mantine's accessibility defaults.
  • Keyboard navigation works without a mouse (Phase 4 work — the table is usable end-to-end via keyboard).

License

MIT — © VSTN Tech.