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

@illumify/react-data-grid

v0.1.0

Published

Virtualized, editable data grid with cell selection, fill handle and column reordering

Readme

DataGrid (lightweight)

A lightweight, virtualized table built on the shared grid protocol (SpreadsheetApiData). It renders the same authored/API column + row model the legacy Spreadsheet consumes, but ships only a thin core; every interactive capability (cell selection, editing, fill handle, column reorder, context menus) is an opt-in, lazily-loaded plugin. Disabled plugins never enter the bundle.

Use this when you need a read-mostly or selectively-interactive grid that stays small. The full Excel-like surface (formulas, validation, the entire toolbar) lives in a separate component that is not yet published as a package.

Install

npm install @illumify/react-data-grid

react (18 or 19) is a peer dependency and must already be in the project. Everything else the grid renders with — MUI, Emotion, the virtualizer, the drag-and-drop adapter, and the cell protocol — travels with it, so <DataGrid data={…} /> renders with no provider and no theme setup.

Quick start

import { DataGrid } from "@illumify/react-data-grid";

<DataGrid data={apiData} />;

data is the only required prop. Everything else is opt-in.

<DataGrid
  data={apiData}
  variant="compact"
  enableSelection
  selectedRowKeys={selectedKeys}
  onSelectionChange={setSelectedKeys}
  getRowKey={({ row, index }) =>
    row.cellList[0]?.value?.toString() ?? String(index)
  }
  enableCellSelection
  enableCellEditing
  enableFillHandle
  enableColumnReorder
  enableUndo
  undoResetKey={sheetId}
  onCellsChange={(changes) => persist(changes)}
  onUndoStateChange={setUndoState}
  onSortChange={(sort) => refetch(sort)}
/>

Public API

Everything below is re-exported from the package barrel (@illumify/react-data-grid). Do not import from internal files.

DataGridProps

| Prop | Type | Default | Notes | | ---------------------------- | ----------------------------------------------- | --------------- | ------------------------------------------------------------------------------ | | data | SpreadsheetApiData | — | Required. Column config + rows. See Data shape. | | variant | DataGridVariant | "spreadsheet" | Preset style token set. See Theming. | | styleOverrides | Partial<DataGridStyleTokens> | — | Per-token overrides merged onto the variant. | | dataAccess | DataGridDataAccess | — | { mode: "all" \| "pagination" \| "infinite" }. | | columnState | ColumnState | uncontrolled | Controlled widths / order / hidden. See Column state. | | onColumnStateChange | (next: ColumnState) => void | — | Fires on resize / reorder / hide. | | rowState | RowState | uncontrolled | Controlled row-height overrides keyed by getRowKey. | | onRowStateChange | (next: RowState) => void | — | Fires when a row border drag commits. | | overlayImagesEditable | boolean | false | Enables selection, dragging, and corner resizing for over-cell images. | | onOverlayImagesChange | (next: OverlayImagesApiConfig) => void | — | Emits the complete updated overlay-image configuration. | | onSortChange | (sort \| null) => void | — | { columnIndex, direction: "asc" \| "desc" } or null when cleared. | | enableSelection | boolean | false | Adds the leading checkbox column + select-all. | | selectedRowKeys | ReadonlySet<string> | — | Controlled row selection. | | onSelectionChange | (keys: ReadonlySet<string>) => void | — | Selection callback. | | getRowKey | ({ row, index }) => string | String(index) | Stable key per row; recommended whenever enableSelection. | | getStickyGroupKey | (context) => string \| number \| null | — | Makes the first row of each contiguous group sticky while its children scroll. | | renderRowActions | ({ row, dataRowIndex }) => ReactNode | — | Trailing per-row actions column. | | renderCellStart | (context) => ReactNode | — | Optional content before the canonical cell value/control. | | renderCellEnd | (context) => ReactNode | — | Optional content after the canonical cell value/control. | | renderCheckbox | (context) => ReactNode | — | Optional lightweight renderer for canonical checkbox cells. | | getCellDataPw | (context) => string \| undefined | — | Adds a stable data-pw to native checkbox/editor controls. | | renderHeaderAction | (context) => ReactNode | — | Optional action at the end of each canonical column header. | | enableCellSelection | boolean | false | Range selection of cells (drag, shift+arrows). | | cellSelection | ApiSelectionRect \| null | — | Controlled cell-selection rect. | | onCellSelectionChange | (next: ApiSelectionRect \| null) => void | — | Cell-selection callback. | | cellSelectionResetKey | string \| number | — | Changing it clears uncontrolled selection/active range and cancels an editor. | | cellSelectionRestrictions | DataGridCellSelectionRestrictions | — | Opt-in header/column exclusions for selection and editing. | | enableCellEditing | boolean | false | In-place editing; emits via onCellsChange. | | enableCellClear | boolean | false | Delete / Backspace clears selected editable non-checkbox cells. | | enableColumnVirtualization | boolean | false | Virtualizes fixed-width body columns while keeping the complete header stack. | | rowOverscan | number | 8 | Extra virtual rows rendered before and after the visible range. | | enableFillHandle | boolean | false | Drag-fill handle (requires range selection). | | enableColumnReorder | boolean | false | Drag column headers to reorder. | | enableUndo | boolean | false | Enables controlled-output undo/redo history. | | undoResetKey | string \| number | — | Changing it clears both undo and redo stacks. | | onUndoStateChange | (state: { canUndo; canRedo }) => void | — | Reports whether undo and redo are currently available. | | onCellsChange | (changes: RowChange[]) => void | — | Edit / fill / checkbox-toggle output. | | cellEditingApiRef | Ref<DataGridCellEditingApi> | — | Imperative active-editor commit/cancel for external actions such as Save. | | onCellEditingChange | (state: DataGridCellEditingState) => void | — | Reports active editor, draft changes, and dirty state. | | validateCellChanges | (changes: RowChange[]) => string \| undefined | — | Keeps an active editor open and displays the returned validation message. | | contextMenuItems | string[] | — | Body context-menu item ids; non-empty enables the plugin. | | contextMenuMode | "editor" \| "runtime" | "runtime" | Context passed to menu resolvers. | | contextMenuRegistry | ContextMenuItem[] | — | Custom item definitions. | | contextMenuActions | Record<string, unknown> | — | Action handlers keyed by item id. | | headerContextMenuItems | string[] | — | Header context-menu item ids; non-empty enables the plugin. | | headerContextMenuRegistry | HeaderContextMenuItem[] | — | Custom header item definitions. | | headerContextMenuActions | Record<string, unknown> | — | Header action handlers. | | onRevealColumns | () => void | — | Invoked when a hidden-column reveal indicator is clicked. | | onRevealRows | () => void | — | Invoked when a hidden-row reveal indicator is clicked. | | dummyColumns | boolean | false | Pads remaining horizontal space with a filler column. |

The cell slot context is { cell, column, row, dataRowIndex }; both slots surround the grid's existing formatter or checkbox instead of replacing it. Interactive cell-slot content must stop propagation when its action should not also select the cell. renderCheckbox receives the same cell context plus { checked, disabled, dataPw, onChange, onMouseDown }; custom controls must wire both handlers so pointer selection and toggling stay batched. The header action context is { cell, column, columnPosition }. The grid isolates the header action's mouse events from sorting, range selection, and column auto-fit.

getCellDataPw receives the same cell context and places the returned value on the native checkbox or active editor element. Use it when browser tests must target the control itself; read-only text cells do not receive an extra wrapper or attribute.

getStickyGroupKey receives { row, dataRowIndex }. Consecutive rows that return the same non-null key form a group; its first row stays below the complete header stack while its remaining rows scroll. Returning null or undefined leaves a row ungrouped and ends the preceding group. The virtualizer retains only the active parent plus the normal viewport, so large groups remain virtualized. Nested and canonical header bands remain layered above horizontally sticky cells and sticky group parents, so scrolling body content cannot paint into the header stack.

cellSelectionRestrictions accepts { excludeHeader, excludedColumnPropertyNames }. Excluded columns cannot become a pointer or keyboard focus target, are skipped by editing, and are not painted as selected. The default remains unrestricted.

cellSelectionResetKey is for data projections whose rendered row or column coordinates can change without remounting the grid. Changing the key clears only the grid's uncontrolled selection and active range, and cancels an open editor so it cannot remain attached to a stale coordinate. A controlled cellSelection remains consumer-owned and is not cleared or emitted by the reset.

Exported types: DataGridProps, DataGridVariant, DataGridDataAccess, DataGridCellSelectionRestrictions, DataGridUndoState, DataGridCellRenderContext, DataGridCheckboxRenderContext, DataGridGetCellDataPw, DataGridHeaderActionRenderContext, DataGridRowContext, DataGridGetStickyGroupKey, DataGridStickyGroupKey, DataGridRenderCellSlot, DataGridRenderCheckbox, DataGridRenderHeaderAction, ColumnState, RowState, RowChange, PluginId, DatagridPluginProps, DataGridStyleTokens, ApiSelectionRect, SelectionRect, CellCoord, SelectableArea, DataGridCellEditingApi, DataGridEditCommitResult, plus the context-menu and visibility types listed below.

Data shape

data follows the cell protocol, re-exported from this package:

  • columnListConfiguration: ApiColumn[] — authored columns. Each carries index, propertyName, displayName, a cell type (numeric / text / date / dropdown / checkbox), and optional width, sticky ("left" | "right"), sortable, sortMode ("client" | "server"), hidden, readOnly, skipOnPaste, headerColor, style, and context (group key/index for nested headers).
  • rows: ApiRow[] — each row has a rowType (Data, Category, SkuClass, Brand, StaticHeader) and a cellList: ApiCell[]. Cells mirror the column's type and hold value, optional style, readOnly, and note.
  • Optional: cellStyles, mergedCellsConfig, nestedHeaders, fixedRowsTop / fixedRowsBottom (pinned rows), overlayImages.

Values created with toCellImageValue(src) render as centered, contained images instead of text. Image cells are read-only through the text editor. Copy/export projects the value to its source URL, and client-side text sorting compares that source URL. overlayImages.images with an inline src render at their absolute pixel geometry over scrolling cells; entries backed only by documentId require the consumer to hydrate src first. Overlay images remain pointer-transparent by default. With overlayImagesEditable, clicking an image selects it, dragging moves it, and four corner handles resize it while preserving its current aspect ratio. Clicking outside deselects it. The editable overlay plane sits above ordinary and sticky body cells, remains independent of row boxes, and stays below the complete sticky header stack. Every pointer move emits a complete replacement OverlayImagesApiConfig through onOverlayImagesChange; the grid never mutates data.overlayImages.

Plain text spills across adjacent empty cells and stops before the next populated cell. Left/default alignment spills right and right alignment spills left. Spills stay within their horizontal plane: body spills scroll with the table, while sticky-left and sticky-right spills stay frozen and stop at their respective freeze boundary. Composed values, merged cells, and cells using start/end render slots retain clipped cell-local rendering.

Sticky-left columns are viewport-aware. The grid reserves a 160px scrolling gutter and freezes only the longest leading sticky prefix that fits before it; later columns authored as sticky: "left" render as ordinary scrolling columns. The effective prefix is recalculated when the scroll container resizes and whenever the visible column configuration changes. Selection and right-side action widths count against the available frozen width.

Cell type and row type literals come from ApiCellType / ApiRowType (as const objects, not enums) in grid-common/protocol/cell-types.

Theming

Styling is token-driven. Pick a variant for a preset, then optionally patch individual tokens with styleOverrides.

  • Variants: spreadsheet (default), compact, comfortable, striped, ocean, forest.
  • DataGridStyleTokens controls border, header background/color, optional column/nested-header heights, cell padding, font size, row height, zebra striping, and selection border/background. headerRowHeight and nestedHeaderRowHeight fall back to rowHeight. rowHeightIncludesPadding, separateBorders, verticalDividers, and preserveSelectionBackground are opt-in and omitted by every preset. separateBorders uses the separate table border model with zero spacing; the grid enables that model automatically whenever it has effective horizontal sticky cells so collapsed-border raster seams cannot reveal scrolling content. Sticky cells always composite their authored paint over an opaque variant surface, even when ordinary cells use transparentBackground. All preset values reference customTheme / palette — never raw hex.
<DataGrid
  data={apiData}
  variant="ocean"
  styleOverrides={{
    preserveSelectionBackground: true,
    rowHeight: 40,
    rowHeightIncludesPadding: true,
    separateBorders: true,
    verticalDividers: true,
    zebra: false,
  }}
/>

Sorting

Sorting is header-driven and tri-state per column (ascdesc → cleared). The grid owns sort UI state and reports changes through onSortChange; null means "no sort." Columns opt in via sortable: true on the ApiColumn. sortMode distinguishes client-side comparison from server-driven sorting — for "server", react to onSortChange by refetching.

Column state

ColumnState is { widths, order, hidden } keyed by propertyName. Pass columnState + onColumnStateChange to control it, or leave both off for internal state.

Column headers expose the normal width-resize handle. With showHeader={false}, the right border of any unmerged body cell becomes the width-resize affordance, including sticky columns.

RowState is { heights } keyed by getRowKey (or the default absolute data-row index). Pass rowState + onRowStateChange to control it, or leave both off for internal state. A stored height overrides row.style.height, so a projection can replace row objects without erasing a user resize. Uncontrolled heights reset when data.spreadsheetId changes. The bottom border of any unmerged body cell is the row-resize affordance; this keeps resizing available without requiring a row-header gutter.

Leave columnState and onColumnStateChange off and the grid keeps column widths, order and visibility in memory for as long as it stays mounted. That state does not survive a reload, and this package ships nothing that makes it: the ERP version was backed by a user-preferences service that is not part of this package.

To persist it, own the state and store it wherever you keep user settings:

import { DataGrid, type ColumnState } from "@illumify/react-data-grid";

const [columnState, setColumnState] = useState<ColumnState>(load());

<DataGrid
  data={apiData}
  columnState={columnState}
  onColumnStateChange={(next) => {
    setColumnState(next);
    save(next);
  }}
/>;

Both props must be supplied together — passing only columnState leaves the grid uncontrolled.

It reads/writes through the shared user-preferences hooks; a storageKey of undefined makes it a no-op.

Visibility (hide / reveal)

Columns and rows can be hidden and revealed via inline reveal indicators. The model helpers are exported for callers that compute visibility outside the grid:

  • applyHide, applyHideRange, applyReveal and the VisibilityMode / VisibilityEntry types.
  • resolveColumnVisibility / resolveRowVisibility (+ their result and indicator types).

The grid surfaces reveal clicks through onRevealColumns / onRevealRows.

Virtualization

Rows are virtualized via TanStack Virtual against the scroll container; only on-screen scrollable rows render. Pinned top/bottom rows (fixedRowsTop / fixedRowsBottom) render in <thead> / <tfoot> outside the virtual window. Row height resolves in this order: a RowState override, authored row.style.height, then the active style token. Authored and user-resized heights are authoritative during DOM measurement, preventing table layout or merged cells from inflating virtual scroll geometry; rows without either may still report dynamic content height.

enableColumnVirtualization additionally limits each body row to the horizontally visible fixed-width columns plus overscan. The complete <colgroup>, canonical header row, and nested headers remain mounted, while omitted body ranges become lightweight colSpan spacers. Sticky columns, the active cell, and selection boundaries are always retained so keyboard navigation, editing, and fill overlays keep their DOM anchors. Percentage-width columns, merged cells, and inline reveal indicators automatically fall back to the complete body renderer.

Coordinate model

There are three coordinate spaces — confusing them is the most common source of off-by-one bugs:

  • Rendered rows — what the <table> paints. If a StaticHeader row exists it is rendered row 0, and data rows start at rendered row 1. Cell selection, fill, and the editor all work in rendered space.
  • Data rows — index into data.rows filtered to rowType === "Data". dataRowIndex = hasHeader ? renderedRow - 1 : renderedRow. This is the index reported by onCellsChange.
  • API rectApiSelectionRect { minRow, maxRow, minCol, maxCol }, the controlled cellSelection / onCellSelectionChange shape.

Always convert through plugins/_shared/coordinate-mapper.ts, and build change payloads through plugins/_shared/build-cells-change.ts (buildCellsChange / buildCellsChangeFromFillResult) — both already apply the header offset and normalize values to string | number | boolean | null. RowChange = { rowIndex, row: Record<propertyName, value>, changes: CellChange[] } where CellChange = { columnIndex, propertyName, value, previousValue }.

Keyboard behavior

With cell selection enabled, arrows move the active cell, Shift+arrows extend the range, and Tab / Shift+Tab move one cell right / left. Navigation keeps the focused cell inside the scroll viewport. A secondary click inside the current range preserves its anchor and extent; a secondary click outside the range collapses selection to that cell. With cell editing enabled, Enter opens an editable cell and moves down when the selected cell is read-only or otherwise non-editable. Printable keyboard input and browser text-input events open the selected editable cell with that text as its replacement value. Space toggles the selected editable checkbox and emits the same RowChange shape as a pointer toggle. Paste distributes TSV values across eligible columns from the selection anchor; readOnly and skipOnPaste columns are skipped without consuming a value. When enableCellClear is on, Delete / Backspace clear selected editable non-checkbox cells through onCellsChange. Inside an active editor, Enter commits and moves down, Tab commits and moves right, Shift+Tab commits and moves left, and Escape cancels. With undo enabled, Cmd/Ctrl+Z emits the inverse batch through onCellsChange; Cmd/Ctrl+Shift+Z re-emits the original batch for redo. Invalid numeric text keeps the editor open and exposes an accessible validation error.

Plugins

The interactive features are lazy controllers mounted only when enabled. The public API is intentionally boolean/array props (no enabledPluginIds escape hatch) — resolveEnabledPlugins derives the single set of active plugins from them:

| Plugin id | Enabled by | | ------------------- | ---------------------------------- | | cellSelection | enableCellSelection | | cellEditing | enableCellEditing | | fillHandle | enableFillHandle | | columnReorder | enableColumnReorder | | contextMenu | non-empty contextMenuItems | | headerContextMenu | non-empty headerContextMenuItems | | undo | enableUndo |

Adding or extending a plugin is documented in PLUGINS.md — including the controller contract, the per-grid plugin store, and the one-line registry entry each plugin requires.

Internal layout (for contributors)

data-grid.tsx              core: composes hooks, renders table, mounts enabled plugin controllers
compute-column-layout.ts   sticky offsets / total width / visible index computation
grid-helpers.ts            cell value formatting
constants.ts               width bounds, empty column state, indicator sizes
column-state/              ColumnState reducer + useColumnPreferences (persistence)
sorting/                   comparison logic
visibility/                hide/reveal model + column/row visibility resolvers
virtualization/            range extractor for the virtualizer
theme/                     style tokens, variants, resolveTokens
hooks/                     use-column-model, use-row-model, use-row-selection,
                           use-grid-virtualization, use-column-virtualization,
                           use-column-resize, use-sticky-config
ui/                        presentational shells (header row, data row, body, colgroup,
                           toolbar, pinned rows, nested headers, rich-text cell)
plugins/                   lazy plugin controllers + runtime (see PLUGINS.md)

The ui/ components are layout shells that receive renderRow(row, index) / renderColumnHeader(cell, posIdx) render-prop callbacks from the core instead of threading per-cell handlers down — see ui/grid-header-row.tsx for the canonical example.

Render pipeline (data-grid.tsx). DataGrid mounts GridPluginStoreProvider (a fresh per-grid Zustand store) and renders DataGridInner, which: (1) calls resolveEnabledPlugins(props) → the active ReadonlySet<PluginId>; (2) resolves tokens; (3) builds the column model (useColumnModel) and row model (useRowModel, applying sortState); (4) builds row + cell selection; (5) reads plugin contributions from the store; (6) memoizes gridApi (shared surface) and pluginOptions (per-plugin); (7) sets up virtualization; (8) renders <table> (colgroup → nested headers → header row → pinned-top → virtualized body → pinned-bottom); (9) mounts each enabled plugin controller in <Suspense> with { api: gridApi, options: pluginOptions[id] }.

Gotchas

  • Header-offset off-by-one. See Coordinate model. Edits/fills landing one row off, or the top data row being skipped, almost always means rendered vs data row confusion — go through the _shared helpers, never hand-roll the offset.
  • Enablement is derived, not declared. There is no enabledPluginIds prop. A feature only activates if resolveEnabledPlugins returns its id — e.g. a context menu needs a non-empty contextMenuItems; passing only a registry does nothing. fillHandle and cellEditing implicitly force cellSelection on.
  • The grid is controlled-output. It never mutates data; it emits RowChange[], selection sets, sort, column state, and reveal events. The consumer applies them. Persist fills/edits into dashboard widgets via mergeFillOverridescellValueOverrides + cellStyles.
  • cellSelection has no controller — it is a core hook gated by effectiveCellSelectionEnabled, present in PLUGIN_IDS only for gating. Don't add a controller for it. (See PLUGINS.md.)
  • Column virtualization is core layout, not a plugin. It is opt-in through enableColumnVirtualization and preserves full protocol coordinates even when an offscreen body cell is not mounted.
  • Store is per-grid. Never introduce a module-level store ref; always read via the useGridPluginStore context hook, and pair every setXxx contribution with a resetXxx cleanup.
  • Don't redefine the protocol or hardcode colors. Reuse ApiColumn/ApiCell/ApiRow/SpreadsheetApiData from the package barrel; style only through tokens/variants. Import from the @illumify/react-data-grid barrel, not internal files. Run npm run typecheck after changes.