data-griddle
v0.1.0
Published
A virtualized React data grid with editing, frozen columns and customizable styles.
Maintainers
Readme
Data Griddle - A performant React Data Grid
A virtualized, DOM-based data grid for React. The component supports large datasets while keeping selection, editing, and pointer interactions off the main cell-rendering path.
Features
- Virtualized rows and center columns
- Left and right frozen columns
- Cell focus, range selection, and checkbox row selection
- Touch hold-and-drag range selection with edge scrolling
- Touch double-tap entry into cell editing
- Keyboard navigation and type-to-edit
- Custom cell, header, editor, and selection-checkbox renderers
- Stable-ID imperative cell focus
- Synchronous validation and asynchronous commits
- Mouse and touch within-zone column reordering (hold a header, drag, release)
- Mouse and touch column resizing with optional persistence callbacks
Usage
The initial release target is 0.1.0, an experimental 0.x release. APIs, styling contracts
and behavior may change; pin an exact version and review release notes before upgrading.
The command below is for the npm release; until it is published, use the local tarball workflow
in the package verification guide.
npm install --save-exact [email protected]Requires matching React and React DOM versions satisfying ^18.3.1 || ^19.2.6.
The package is ESM-only and targets ES2022; CommonJS is not provided. Packed consumers have been
tested with React 18.3.1 and 19.2.6 in Vite, Next.js Pages Router, and React 19 in Next.js App Router.
These checks use desktop Chromium; they do not establish minimum browser/OS versions or full
mobile and screen-reader support.
Import data-griddle/styles.css once in your application entry. The JavaScript entry does not
automatically inject CSS. In Next.js use the root layout (App Router) or pages/_app.tsx.
"use client";
import "data-griddle/styles.css";
import { useState } from "react";
import { DataGrid } from "data-griddle";
import type { CellCommit, Column } from "data-griddle";
type Person = {
id: number;
name: string;
role: string;
};
const columns: Column<Person>[] = [
{
id: "name",
name: "Name",
width: 220,
frozen: "left",
accessor: (row) => row.name,
editable: true,
},
{
id: "role",
name: "Role",
width: 180,
accessor: (row) => row.role,
editable: true,
},
];
export function PeopleGrid() {
const [rows, setRows] = useState<Person[]>([
{ id: 1, name: "Ada", role: "Engineer" },
]);
const [columnOrder, setColumnOrder] = useState(
columns.map((column) => column.id)
);
const commit = ({ rowId, columnId, nextValue }: CellCommit<Person>) => {
setRows((current) =>
current.map((row) =>
row.id === rowId ? { ...row, [columnId]: nextValue } : row
)
);
};
return (
<div style={{ height: 480 }}>
<DataGrid
rows={rows}
columns={columns}
getRowId={(row) => row.id}
columnOrder={columnOrder}
onColumnOrderChange={setColumnOrder}
onCellCommit={commit}
enableRowSelection
/>
</div>
);
}The grid fills its parent, so its container must have a defined height. Row data remains owned by
the caller; commit handlers must update rows with accepted values.
Loading and empty results
Pass application-owned request activity through loading (default false). The required rows
prop accepts null and undefined for an unavailable result; an empty array means a completed
result with no rows. Preserve that distinction instead of replacing missing data with [].
<DataGrid
rows={data}
loading={isFetching}
columns={columns}
getRowId={(row) => row.id}
aria-label="People"
loadingLabel="Updating people"
emptyContent={<span>No matching people</span>}
/>Here data and isFetching come from your application. The grid does not fetch or cache data.
| Rows | Loading | Display |
| -------------------- | ------- | ---------------------------------------------------------------------------- |
| null / undefined | true | Headers and skeleton rows, without a spinner |
| null / undefined | false | Headers and a blank body; the app handles errors or instructions |
| Any array | true | Retained result beneath an overlay covering headers and body, with a spinner |
| [] | false | Empty content, defaulting to “No rows” |
| Populated array | false | Normal grid |
Keep supplying previous rows during sorting, filtering, or pagination requests to retain them under
the overlay. Passing missing rows removes that result and shows the initial skeleton while loading.
Initial skeleton bars gently pulse to show activity. The existing skeleton fallback for virtualized
rendering gaps stays static and works independently of loading.
loadingIndicator replaces only the refresh indicator; the grid owns overlay placement and behavior.
emptyContent replaces empty-result content. Explicit null hides either visual. Keep custom
indicators noninteractive; loadingLabel supplies the accessible announcement independently.
The default spinner always rotates, including when reduced motion is enabled. Custom indicators
control their own animation behavior.
Loading blocks pointer and keyboard interactions inside the grid, including custom cell/header controls and the editor portal. Outside controls remain usable. Active resize/reorder gestures are cancelled without committing, selection stops at its current range, and pending saves finish normally. An active editor is paused with its draft intact and resumes against the same row ID. If the edited row or column disappears, a notice preserves the draft until its target returns or you explicitly discard it. Focus is restored after a background refresh unless you moved it to another control. As usual, an outside click before loading begins may commit an edit.
id, className, and style apply to the stationary outer frame; accessible naming applies to the
focusable inner scroller. Code that previously used the root element as the scroll container must
account for this new wrapper. Loading styles are included in the exported stylesheet. Customize their
colors through CSS variables on the frame:
.my-grid {
--dgr-loading-overlay-background: rgb(255 255 255 / 65%);
--dgr-loading-indicator-color: #57534e;
--dgr-empty-color: #78716c;
}The packed-consumer fixture exercises initial loading, refresh and empty results. Custom indicators and delayed refresh while editing need a dedicated consumer scenario; the local app is a basic grid.
Column behavior
Explicit column widths are preserved, even when they leave unused space. The last column in visual
order without a width or a width override fills the remaining viewport width; earlier unspecified
columns use the 140px default. The available space excludes the checkbox gutter and all other
columns, including frozen columns. Automatic sizing respects minWidth (48px by default) and
maxWidth: insufficient room causes horizontal scrolling, while a maximum can leave unused space.
Manually resizing an automatic column gives it an explicit width for the session. The last remaining
unspecified column then fills the space, if there is one. Controlled columnWidths and
defaultColumnWidths also count as explicit widths. Removing a controlled override restores the
column's eligibility for automatic sizing when its schema has no width. Container-driven sizing
does not emit onColumnWidthsChange.
accessorreads the displayed value from a row.editableenables editing;renderEditorcan provide a custom editor.renderCellandrenderHeadercustomize read-mode cells and headers.parseValueconverts a draft before validation and commit.validatereturns an error message to reject a value.onCommitoverrides the grid-levelonCellCommitfor one column.frozenpins a column to the left or right zone.selectable,resizable,reorderable, andreorderBarriercustomize normal-column behavior.type: 'action'is always non-selectable, non-editable, non-resizable, non-reorderable, and a reorder barrier; explicit capability props cannot override those invariants.
Row selection, column order, and column widths support controlled and uncontrolled use through
value/defaultValue/onChange-style prop groups. Reordering and resizing work internally by
default. A controlled value without its change callback is read-only and its matching affordances
are disabled.
Frozen columns are always rendered rather than horizontally virtualized. Keep the number frozen on each side small for large grids.
Development
pnpm install --frozen-lockfile
pnpm dev # library watch + single-grid Next.js app
pnpm test # source regressions
pnpm lint # ESLint + library/app/fixture types
pnpm build # ESM library + static single-grid app
pnpm check:package # build + artifact contract
pnpm exec playwright install chromium
pnpm check:consumers # real tarballs in isolated Vite/Next consumers
pnpm check:touch # Chromium touch gestures; requires pnpm dev in another terminalSee INTERNALS.md for the rendering model and performance constraints.
For phone testing through ngrok, put the tunnel hostname (without https:// or a port) in
apps/docs/.env.local, then restart pnpm dev:
DEV_TUNNEL_HOST=your-tunnel.ngrok-free.devThe development app uses this hostname in Next.js allowedDevOrigins so its dev WebSocket can
connect through the tunnel. Without it, the page can remain on its server-rendered skeleton.
Update the value if your tunnel hostname changes. The local environment file is Git-ignored.
Styling and themes
Default styles are scoped to grid-owned classes; import data-griddle/styles.css. Set
--dgr-* variables on the frame or an ancestor; active body-mounted editors copy their originating
grid's theme. Columns accept cellClassName and headerClassName as strings or context callbacks.
Cells and headers clip overflow without automatic ellipsis. Opt into truncation with application
CSS through these classes, or style individual labels and links inside custom renderers.
Use rowHeight and column widths for geometry; CSS controls appearance.
See the styling contract for tokens, defaults, parts, callback contexts, and portal synchronization limits. The guide includes theme and conditional-class examples; the local app does not include an interactive theme showcase.
Integration APIs
See the integration guide for controlled/uncontrolled/read-only state,
stable identity, editing and persistence, custom selection controls, and the DataGridHandle.focusCell
command. Run pnpm dev and open / for the basic editable grid. Packed-consumer fixtures cover
additional public-API scenarios; their setup and coverage are described in the verification guide below.
Stores and geometry helpers remain private. See package verification for exact tested versions and remaining limitations.
Accessibility and interaction
See keyboard, touch and accessibility guidance for navigation, native control tab order, editor behavior, remaining touch/column interaction gaps and the manual test matrix. Source semantics and keyboard navigation are implemented; target-browser, physical-device and screen-reader support remains unverified. Applications must adapt frozen widths and columns to narrow containers; the basic app does not demonstrate responsive unpinning.
Workspace and releases
packages/data-griddle/: canonical source, styles, tests, ESM build and declarations.apps/docs/: a single-grid Next.js App Router development app.fixtures/: isolated packed consumers, copied outside the workspace by verification scripts.docs/: repository guides, read directly as Markdown.
The development app consumes only public package exports. pnpm dev builds the library first and watches
it alongside Next.js; restart it after changing the public export map. No Turborepo or Nx is needed.
The initial release uses version 0.1.0 and the npm latest tag. The tag controls the default
installation; it does not imply a stable API. Pin preview versions exactly. Documentation lives
in this README and the repository guides; a dedicated documentation site is outside this release.
See the release checklist for preparation status, remaining verification and the manual publishing procedure, and release notes for the initial feature scope and limitations. The library is configured for publication; the workspace and development app remain private. There is no automated publishing workflow.
