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

react-glide-table

v2.1.0

Published

Headless React table built on [@tanstack/react-table](https://tanstack.com/table) and [@tanstack/react-virtual](https://tanstack.com/virtual).

Readme

react-glide-table

Headless React table built on @tanstack/react-table and @tanstack/react-virtual.

Primary DX is a compound API (createTable / Table.Column) with an unstyled semantic HTML shell. Customize with className, classNames (Tailwind-friendly part map), slots, and column render. useGlideTable remains the lower-level escape hatch when you need full control of markup.

The package ships no CSS — class names like DataTableJSX / data-table are opt-in hooks for your own styles.

Installation

npm install react-glide-table
# or
pnpm add react-glide-table

Peer dependencies: react and react-dom (^18 or ^19).

The package is marked "sideEffects": false for tree-shaking. Prefer subpath imports when you only need one surface:

| Import | Contents | | ---------------------------- | -------------------------------------------------------- | | react-glide-table | Full barrel (compound + core) | | react-glide-table/compound | createTable / Table / DataTable + related types | | react-glide-table/core | useGlideTable + feature hooks/helpers (no compound UI) |

Quick start (compound)

import { createTable } from "react-glide-table/compound";
import { useState } from "react";

type Product = { id: string; name: string; qty: number };

const ProductTable = createTable<Product>();

export function Products({ data }: { data: Product[] }) {
  const [page, setPage] = useState(1);

  return (
    <ProductTable
      data={data}
      getRowId={(row) => row.id}
      className="rounded-lg border"
      classNames={{
        scroll: "max-h-[480px] overflow-auto",
        head: "sticky top-0 z-10 bg-neutral-50",
        headCell: "px-3 text-xs font-semibold text-neutral-500",
        row: "hover:bg-neutral-100 data-[selected]:bg-blue-600 data-[selected]:text-white",
        cell: "border-b border-neutral-200 px-3 text-sm",
        toolbar: "flex items-center justify-between gap-2 p-2",
      }}
    >
      <ProductTable.Header>
        <ProductTable.Column field="name">Name</ProductTable.Column>
        <ProductTable.Column field="qty" editable>
          Qty
        </ProductTable.Column>
      </ProductTable.Header>
      <ProductTable.Pagination page={page} pageSize={10} onChange={setPage} />
    </ProductTable>
  );
}

Customization surface

| Slot / prop | Role | | ---------------------------------------------------- | --------------------------------------------------------------------------------- | | classNames | Per-part Tailwind/utility classes (root, scroll, row, cell, toolbar, …) | | slots.Toolbar | Top summary / actions region | | slots.Scroll | Scroll container (must attach scrollRef to the real overflow element) | | slots.Row | Full row replacement (cells, selection, edit UI) | | slots.Pending / slots.Empty | Loading and empty states | | className / column className / headerClassName | Extra class hooks | | labels / summary / toolbar | Copy and slot nodes | | Column.render / ColumnDef.cell | Cell content custom render (prefer update via context) | | Column.kind / cellRenderers | Built-in or custom cell kinds (override / add via registry) |

Row/cell state is exposed as data-* attributes for Tailwind variants:

  • row: data-selected, data-hovered, data-expandable, data-expanded
  • cell: data-merged, data-selection-fill, data-editable, data-editing, data-frozen, data-search-match, data-search-active, …

Example: row: "data-[selected]:bg-blue-600".

Row-level UI → slots.Row. Cell content → Column.render or Column.kind. Header/Cell are not separate slots.

slots.Scroll receives { scrollRef, className, children }. Attach scrollRef to the element that owns overflow scrolling — virtualization depends on it. Styling-only changes can stay on classNames.scroll.

Cell render context (Column.render)

Breaking change (v2.0.0): render receives one context object (not positional args). Destructure only what you need.

<ProductTable.Column
  field="status"
  render={({ value, update }) => (
    <button type="button" onClick={() => update("done")}>
      {String(value)}
    </button>
  )}
/>

update commits through onCellChange (preferred) or onDataChange. Prefer update over calling setData inside the render.

The same update is available on TanStack ColumnDef.cell when using DataTable directly (or useGlideTable().getCellContext with flexRender). Bare cell.getContext() does not include update at runtime — only the wrapped context does (CellContextWithUpdate).

const columns: ColumnDef<Product, unknown>[] = [
  {
    accessorKey: "status",
    header: "Status",
    cell: ({ getValue, update }) => (
      <button type="button" onClick={() => update?.("done")}>
        {String(getValue())}
      </button>
    ),
  },
];

<DataTable data={data} columns={columns} onCellChange={...} />

Double-click inline editing (editable / editType) stays separate: use it for overlay text/number edits; use render / cell + update for always-visible controls.

Cell kinds (Column.kind + cellRenderers)

Built-ins (DOM, inspired by Glide All Cell Kinds): text, number, boolean, uri, image, bubble, markdown, drilldown, loading, protected, row-id.

Priority: column render > registry kind > default text.

const buttonRenderer = {
  kind: "my-button",
  render: ({ value, update }) => (
    <button type="button" onClick={() => update("done")}>
      {String(value)}
    </button>
  ),
};

<ProductTable cellRenderers={[buttonRenderer]}>
  <ProductTable.Header>
    <ProductTable.Column field="active" kind="boolean">
      Active
    </ProductTable.Column>
    <ProductTable.Column field="action" kind="my-button">
      Action
    </ProductTable.Column>
  </ProductTable.Header>
</ProductTable>

Same kind in cellRenderers overrides the builtin. Extra kinds are added by key.

Escape hatch (useGlideTable)

import { flexRender } from "@tanstack/react-table";
import { useGlideTable } from "react-glide-table/core";
import type { ColumnDef } from "react-glide-table/core";

type Product = { id: string; name: string; qty: number };

const columns: ColumnDef<Product, unknown>[] = [
  { accessorKey: "name", header: "Name" },
  {
    accessorKey: "qty",
    header: "Qty",
    cell: ({ getValue, update }) => (
      <button type="button" onClick={() => update?.(Number(getValue()) + 1)}>
        {String(getValue())}
      </button>
    ),
  },
];

export function ProductTable({ data }: { data: Product[] }) {
  const { table, rows, scrollRef, getCellContext } = useGlideTable({
    data,
    columns,
    getRowId: (row) => row.id,
    rowSelectionMode: "multi",
    onCellChange: (rowId, columnId, value) => {
      /* update your data */
    },
  });

  return (
    <div ref={scrollRef}>
      <table>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th key={header.id}>
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext(),
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {rows.map((row) => (
            <tr key={row.id}>
              {row.getVisibleCells().map((cell) => (
                <td key={cell.id}>
                  {flexRender(cell.column.columnDef.cell, getCellContext(cell))}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Wire rowContextValue into your own row/cell components for edit, selection, expand, and row-span behavior. Use getCellContext(cell) (not bare cell.getContext()) so ColumnDef.cell receives update.

Clipboard copy & paste

Cell selection ships with clipboard shortcuts. The table parses TSV and emits structured payloads; your app applies domain conversion and updates data.

| Shortcut | Behavior | | ---------------- | -------------------------------------------------------------------------------------- | | Ctrl/Cmd+C | Copy the active selection (visible rows) | | Ctrl/Cmd+Shift+C | Copy including collapsed tree descendants (enableSubtreeCopy) | | Ctrl/Cmd+V | Paste overwrite into the selection (onRowsPaste, mode: "overwrite") | | Ctrl/Cmd+Shift+V | Paste insert rows after the selection (mode: "insert", when enableInsertPaste) |

Subtree copy encodes relative tree depth as leading tabs in the TSV so paste can rebuild parent/child nesting via payload.depths. Depth is only inferred when the clipboard looks like subtree indentation (first row unindented, at least one later row indented). Otherwise leading empty cells are kept as real values (e.g. Excel/Sheets blank first column) and depths stay 0.

import type { RowsPastePayload } from "react-glide-table/compound";

<ProductTable
  data={data}
  getRowId={(row) => row.id}
  enableCellSelection
  onRowsPaste={(payload: RowsPastePayload) => {
    // overwrite: update cells from startRow using payload.values / columnIds
    // insert: create rows after payload.endRow (or payload.anchorRowId for trees)
    setData((prev) => applyMyPaste(prev, payload));
  }}
>
  {/* columns… */}
</ProductTable>;

Related props: onRowsPaste, enableInsertPaste, enableSubtreeCopy, onCopyActionsReady.
Helpers (/core): buildRowsPastePayload, parseClipboardTSV, parseClipboardTSVWithDepths, serializeSelectionToTSV, …

Column resize

Opt in with enableColumnResize. Drag the handle on the right edge of a header cell; double-click resets to the column’s default width / size.

<ProductTable
  data={data}
  enableColumnResize
  // optional controlled sizing
  // columnSizing={sizing}
  // onColumnSizingChange={setSizing}
>
  <ProductTable.Header>
    <ProductTable.Column field="name" width={200} minWidth={80} maxWidth={480}>
      Name
    </ProductTable.Column>
    <ProductTable.Column field="sku" resizable={false}>
      SKU
    </ProductTable.Column>
  </ProductTable.Header>
</ProductTable>

| Prop | Role | | ---------------------------------------- | -------------------------------------------- | | enableColumnResize | Turn on header drag resize (default false) | | columnSizing / onColumnSizingChange | Controlled width map { [columnId]: px } | | columnResizeMode | "onChange" (live) or "onEnd" | | Column.width / minWidth / maxWidth | Default / clamp sizes | | Column.resizable={false} | Disable resize for one column | | classNames.resizeHandle | Style hook for the drag handle |

Column reorder

Opt in with enableColumnReorder. Drag a column header to a new position; a drop edge marks the insert point (demo). Movement below a small threshold still counts as a click, so sortable headers keep working. The resize handle does not start a reorder.

Leaf columns can move across ColumnGroups. Consecutive leaves that still share a group stay wrapped; interleaved leaves split that group (Glide-style). Frozen columns keep their freeze after the move.

<ProductTable
  data={data}
  enableColumnReorder
  // optional controlled order (leaf column ids)
  // columnOrder={order}
  // onColumnOrderChange={setOrder}
>
  <ProductTable.Header>
    <ProductTable.Column field="name" width={200}>
      Name
    </ProductTable.Column>
    <ProductTable.Column field="sku" reorderable={false}>
      SKU
    </ProductTable.Column>
  </ProductTable.Header>
</ProductTable>

| Prop | Role | | --------------------------------------- | ------------------------------------------------- | | enableColumnReorder | Turn on header drag reorder (default false) | | columnOrder / onColumnOrderChange | Controlled leaf id list (string[]) | | Column.reorderable={false} | Disable drag for one column (still a drop target) | | data-reorderable / data-drop-edge | Drag / drop state hooks on header cells | | classNames.dropEdge | Extra class while a drop edge is shown |

Helpers (/core): applyLeafColumnOrder, moveColumnIds, collectLeafColumnIds, useColumnReorder, …

Column freeze

Opt in with enableColumnFreeze. Mark columns with frozen — sticky insets are stacked so frozen cells never overlap, and column order is unchanged (middle columns may also freeze).

Keep vertical stickiness on the header via classNames.head (e.g. sticky top-0). Frozen header cells only stick horizontally (left / right); setting top on both thead and frozen th can detach the freeze band.

<ProductTable data={data} enableColumnFreeze>
  <ProductTable.Header>
    <ProductTable.Column field="name" frozen width={200}>
      Name
    </ProductTable.Column>
    <ProductTable.Column field="sku">SKU</ProductTable.Column>
    <ProductTable.Column field="qty" frozen="left">
      Qty
    </ProductTable.Column>
    <ProductTable.Column field="status" frozen="right">
      Status
    </ProductTable.Column>
  </ProductTable.Header>
</ProductTable>

| Prop | Role | | ---------------------------------- | --------------------------------------- | | enableColumnFreeze | Turn on sticky freeze (default false) | | Column.frozen / meta.frozen | true / "left" or "right" | | data-frozen | "left" / "right" on frozen cells | | data-freeze-edge | "left" / "right" / "both" on island boundaries |

Helpers (/core): buildColumnFreezeOffsets, getColumnFreezeEdgeAttr, getColumnFreezeStyle, resolveColumnFreezeSide.

Contiguous same-side freezes share one island (shadow only on the outer boundary). A gap between frozen columns creates separate islands, so both sides of the gap get an edge shadow.

Built-in search (find in page)

Opt in with enableInlineSearch for a Glide Data Grid–style search overlay (demo). This highlights matching cells and navigates between them — it does not filter rows.

On tree tables (toggleField), search also scans collapsed descendants. Navigating to a collapsed match expands the ancestor path so the cell becomes visible.

<ProductTable data={data} enableInlineSearch>
  {/* columns… */}
</ProductTable>

| Shortcut / control | Behavior | | ------------------ | -------- | | Ctrl/Cmd+F | Open (or focus) the search box | | Enter / ↓ | Next match | | Shift+Enter / ↑ | Previous match | | Escape / Ctrl+F (in box) / ✕ | Close |

| Prop | Role | | ---- | ---- | | enableInlineSearch | Turn on search (default false) | | showSearch / onSearchClose | Controlled overlay visibility | | searchValue / onSearchValueChange | Controlled query | | searchResults / onSearchResultsChanged | Override / observe matches ([colIndex, rowIndex]) | | data-search-match / data-search-active | Match highlight hooks on cells | | classNames.search* | Style the overlay |

Helpers (/core): useInlineSearch, collectSearchMatchesInRange, formatSearchResultLabel, …

Public API

| Export | Path | Role | | ------------------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------- | | createTable / Table | /compound | Compound column DSL (Header / Column / Body / Pagination) | | DataTable | /compound | Unstyled default renderer (semantic HTML + slots/props) | | useGlideTable | /core | Headless engine escape hatch | | useCellEdit / useCellSelection / useConvertTreeData / useInlineSearch | /core | Feature hooks | | applyCellEdit, applyFillData, buildRowsPastePayload, buildColumnRowSpanMap, … | /core | Pure helpers | | DEFAULT_DATA_TABLE_LABELS / resolveDataTableLabels | /core | Optional English UI copy helpers | | Tree field defaults | /core | id / parentId / children / qty |

Root react-glide-table re-exports both surfaces. Related types: TableProps, TableColumnProps, DataTableProps, DataTableSlots, DataTableScrollSlotProps, TableCompoundComponent, ColumnDef, RowsPastePayload, PasteMode, …

Notable constraints

  • Row span + virtualization: when enableRowSpan is on, virtualization is forced off (HTML <table> + rowspan cannot safely share a virtual window).
  • Paste is app-owned: the library does not mutate data on paste — handle onRowsPaste (coerce types, ids, tree shape, row-span keys).
  • Flat tree parent order: useConvertTreeData attaches each child to the nearest preceding row whose toggle key matches parentId (duplicate keys after paste resolve this way). Flat inputs must list parents before their children; a child whose parent appears later becomes a root. Nested children arrays are flattened parent-before-child automatically.
  • No shipped CSS: the default renderer emits class hooks only. Bring your own styles (see playground for a CSS-skinned example).

Local playground

pnpm install
pnpm dev

The playground uses createTable with a local CSS skin (src/styles/index.css) to exercise the compound API and headless core — including cell copy/paste (overwrite + insert) on flat and tree (BOM) tables.

License

MIT