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

@twentygx/tablegx

v3.0.0

Published

High-performance, type-safe React data-table system: ReadOnlyTable, EditableTable, and TabbedTable over one virtualized engine.

Downloads

813

Readme

tablegx

High-performance, type-safe React data tables: ReadOnlyTable, EditableTable, TabbedTable, and IndependentTabbedTable, all layered over one virtualized engine.

  • Fast by construction — row virtualization (TanStack Virtual), manual column virtualization for the scrollable pane, DOM-free text measurement (@chenglou/pretext) so auto column widths are computed before paint with zero layout shift, and no setTimeout/requestAnimationFrame timing hacks anywhere.
  • Feature-complete — sorting (single + shift-click multi with priority badges), per-column filter popovers (text search + faceted checklist), removable filter badges, frozen columns (split pinned/scroll panes; auto-sized pinned width caps at 50% of the viewport until you resize a pinned column, then the pane can grow wider), row selection with parent/child/indeterminate semantics, nested rows, inline editing, declarative cell action buttons, footer aggregates, column visibility persistence, column resizing, loading/empty/submitting states.
  • Composable styling — Tailwind CSS v4 utilities + shadcn-style CSS-variable tokens. Every visual region accepts class overrides; theming is just overriding variables.

Install

npm i @twentygx/tablegx
# peers
npm i react react-dom

If you use an AI coding agent, run npx @tanstack/intent@latest install after install to wire TableGX skills into your agent config.

Tailwind CSS v4 is a peer of the styling story: the package ships untranspiled Tailwind classes, so your app's Tailwind build must scan it. In your Tailwind CSS entry:

@import 'tailwindcss';

/* 1. Let Tailwind see tablegx's class names */
@source "../node_modules/@twentygx/tablegx/dist";

/* 2. Default theme tokens (skip if you already define shadcn-style tokens) */
@import '@twentygx/tablegx/theme.css';

theme.css declares every token with zero-specificity :where(...) selectors, so any --background, --primary, etc. you define in your own :root / .dark blocks win automatically. Dark mode follows the standard .dark class convention.

TableGX — the recommended entry point

TableGX is the single component most apps should reach for. It's a thin preset facade over the compound primitives + shared headless store: one import covers every table mode, chosen with a discriminated variant prop. Each variant exposes exactly the props that mode supports (full autocomplete, no nonsensical cross-variant combinations).

import { TableGX, textColumn } from '@twentygx/tablegx'

// One table — read-only or editable, flipped live from your own state.
const [editable, setEditable] = useState(false)

<TableGX<Facility>
  variant="table"
  data={facilities}
  columns={columns}
  getRowId={(r) => r.id}
  editable={editable}              // toggle inline editing on/off live (no remount)
  editableColumnIds={['dba', 'beds']}
  onSaveEdit={save}
/>

// Shared-dataset tabs (cross-tab filter intersection + shared selection/sort).
<TableGX<Facility> variant="tabbed" data={facilities} getRowId={(r) => r.id} idColumn="id" tabs={tabs} />

// Fully independent per-tab tables.
<TableGX variant="independent" tabs={independentTabs} />

Driving editable from React state flips a single table between display-only and inline-editing with no remount (scroll position and selection are preserved); flipping it off mid-edit cancels any in-progress edit so no editor is left stranded. When editable is true but onSaveEdit / editableColumnIds are missing, TableGX warns in development.

Need to rearrange the chrome (move the toolbar, split the tab strip, etc.)? Drop down to the primitives — TableGX.Provider, TableGX.Container, TableGX.TabStrip, TableGX.Panels, TableGX.Body, TableGX.Toolbar, … (the same family as the standalone Table namespace). The focused components below (ReadOnlyTable, EditableTable, TabbedTable, IndependentTabbedTable) remain exported and supported.

Quick start

import { ReadOnlyTable, textColumn, numberColumn, dateColumn } from '@twentygx/tablegx'

type Facility = { id: string; name: string; beds: number; opened: string }

const columns = [
  textColumn<Facility>('name', 'Name', { footerLabel: 'Totals' }),
  numberColumn<Facility>('beds', 'Beds', { footerAggregate: 'sum' }),
  dateColumn<Facility>('opened', 'Opened'),
]

<ReadOnlyTable<Facility>
  data={facilities}
  columns={columns}
  getRowId={(r) => r.id}
  frozenColumns={1}
  enableRowSelection
  enableFooter
  enableColumnVisibility
  columnVisibilityStorageKey="facilities"
  maxHeight="70vh"
/>

Inline editing

import { EditableTable, textColumn, booleanColumn } from '@twentygx/tablegx'

<EditableTable<Facility>
  data={facilities}
  columns={columns}
  getRowId={(r) => r.id}
  editableColumnIds={['dba', 'beds', 'isActive']}
  onSaveEdit={async (row, columnId, value) => {
    const ok = await api.patch(row.id, { [columnId]: value })
    return ok // false keeps the editor open
  }}
  singleClickEdit
/>

A column is editable only when both meta.editable: true is set on the column and its id is in editableColumnIds. Editor type comes from meta.inputType ('text' | 'number' | 'boolean' | 'select'). Keyboard: Enter commits (Shift+Enter inserts a newline in text cells), Escape cancels, blur commits, Tab / Shift+Tab commits and moves to the adjacent editable cell. With singleClickEdit, boolean cells become directly interactive checkboxes.

Tabbed views

import { TabbedTable } from '@twentygx/tablegx'

<TabbedTable<Facility>
  data={facilities}
  getRowId={(r) => r.id}
  idColumn="id"
  tabs={[
    { id: 'overview', label: 'Overview', columns: overviewColumns, frozenColumns: 1 },
    { id: 'edit', label: 'Edit', columns: editColumns, editable: true,
      editableColumnIds: ['dba'], onSaveEdit: save },
  ]}
  enableRowSelection
  columnVisibilityStorageKeyBase="facilities-tabs"
/>

Filters set on any tab restrict every tab to the intersection of passing rows; the shared badge strip shows each filter with its originating tab. Selection and sorting are shared across tabs — a sort applied on one tab carries to every tab, and sort entries for columns a tab doesn't have are simply ignored there. The shared sort is seeded from the initially-active tab's initialSorting (falling back to the first tab that defines one). During the tab slide animation, the frozen pane stays visually static (counter-translated).

Independent tabbed tables

When each tab is a completely separate table — its own data, row shape, identity, columns, and independent sorting/filtering/selection/visibility — use IndependentTabbedTable. Tabs share only the tab-strip shell and slide animation; nothing crosses between them (no idColumn, no cross-tab filter intersection). Build each tab with the independentTable<TRow>() factory, which erases the row type so heterogeneous tabs can live in one array:

import { IndependentTabbedTable, independentTable } from '@twentygx/tablegx'
import type { IndependentTab } from '@twentygx/tablegx'

const tabs: IndependentTab[] = [
  independentTable<Person>({
    id: 'people', label: 'People', data: people, getRowId: (r) => r.id,
    columns: personColumns, enableRowSelection: true,
    enableColumnVisibility: true, columnVisibilityStorageKey: 'app-people',
  }),
  independentTable<Invoice>({
    id: 'invoices', label: 'Invoices', data: invoices, getRowId: (r) => r.id,
    columns: invoiceColumns, editable: true, editableColumnIds: ['amount'],
    onSaveEdit: saveInvoice,
  }),
]

<IndependentTabbedTable tabs={tabs} defaultTabId="people" />

Per-tab sorting, filters, selection, and column visibility are lifted by tab id, so each tab's state survives switching away and back. Chrome (filter badges, column picker, loading/empty states) reflects only the active tab. Column visibility persists under a full columnVisibilityStorageKey per tab (not a shared base). Because frozen columns aren't shared between tabs, the frozen pane slides out together with the scrolling pane during the tab transition (unlike TabbedTable, where the shared frozen pane stays visually static).

Column metadata (meta)

All custom per-column behavior lives in the column def's meta (typed via module augmentation as TableColumnMeta):

| Key | Purpose | | --- | --- | | editable, inputType, selectOptions | Inline editing | | measureText(row) | String to measure for non-text cells (badges, custom renders) | | fixedMeasureWidth | Fixed content width (px); skips sampling (icon/action columns) | | maxColumnWidth | Per-column auto-size clamp (the measured header width is always the floor) | | footerAggregate (sum\|avg\|min\|max\|count), footerFormat, footerLabel | Footer row | | actions | Declarative cell action buttons (isHidden, isDisabled, confirm, tooltip, async busy state, click isolation) |

Column factory helpers (textColumn, numberColumn, booleanColumn, selectColumn, dateColumn, badgeColumn) wire up sensible cells, the default filter, and measurement hints. Plain TanStack ColumnDefs work too — set enableColumnFilter: true to opt a column into the filter popover.

Nested rows

<ReadOnlyTable
  data={tree}
  enableExpanding
  getSubRows={(row) => row.children}
  defaultExpanded            // or { [rowId]: true }
  onExpandedChange={(next) => ...}
  ...
/>

Only visible (expanded) rows hit the virtualizer. Filtering keeps a parent visible when any descendant matches and auto-expands it; selection cascades to descendants with indeterminate parent states; an expand/collapse-all affordance renders in the toolbar.

Dates

Date-only strings ("YYYY-MM-DD") are parsed at midnight UTC and formatted with UTC fields (formatDateSafe / parseDateSafe), so a stored 2024-01-01 never renders as 12/31 in western timezones.

Footer semantics

Aggregates compute over the currently-filtered leaf rows — collapsed-but-matching leaves still count, so totals don't jump when toggling disclosure.

Customization

  • classNames slot object on every component (root, toolbar, filterBadges, headerRow, headerCell, groupHeaderCell, bodyRow, bodyCell, footerRow, footerCell, empty, skeleton, plus container/tabStrip/tab/activeTab/inactiveTab/tabIndicator/panel on TabbedTable). Caller classes win (merged with tailwind-merge).
  • getCellClassName(row, columnId) for per-cell conditional styling (e.g. pending-edit highlights).
  • All colors flow through CSS variables; override --primary, --card, --tgx-header-bg, --tgx-row-hover-bg, --tgx-row-selected-bg, … to retheme.
  • Stable data-attribute hooks for plain-CSS theming (e.g. backdrop blurs on translucent themes): [data-tgx-table], [data-tgx-tabbed-table], [data-tgx-toolbar], [data-tgx-header-block] (the sticky header), [data-tgx-footer-row], [data-tgx-pinned] (every frozen pane: header, body rows, footer), [data-tgx-tab-strip], [data-tgx-row], [data-tgx-cell], [data-tgx-header], [data-tgx-pop] (popovers/menus), [data-tgx-dialog]. If you give the header/pinned surfaces translucent backgrounds, pair them with a backdrop-filter on [data-tgx-header-block] / [data-tgx-pinned] so rows scrolling behind them don't bleed through.
  • measure?: (text, font) => number lets you inject a custom text measurer (also how tests stub measurement).

SSR

The package is client-only ("use client" is preserved in the bundle). Text measurement needs canvas, so on the server columns render at the fallback minimum width and re-measure in a pre-paint layout effect during hydration — no visible shift. window / localStorage / ResizeObserver access is guarded throughout.

Constants

Fixed layout invariants (exported): ROW_HEIGHT_PX (56), HEADER_HEIGHT_PX (48), MIN_COLUMN_WIDTH_PX (160), ABSOLUTE_MIN_COLUMN_WIDTH_PX (48), FROZEN_PANE_MAX_FRACTION (0.5 — caps combined auto-sized pinned width until the user resizes any pinned data column; after that, the frozen pane may exceed this fraction), INDENT_STEP_PX (20), MAX_COLUMN_WIDTH_PX (480).

License

MIT