@wjoffe93/gridline
v1.0.0
Published
A fast, virtualized data grid for React — sorting, filtering, inline editing, undo/redo, and CSV export out of the box.
Maintainers
Readme
gridline
A fast, virtualized data grid for React — sorting, filtering, inline editing, undo/redo, and CSV export without a framework tax.
Why gridline?
Most data grid libraries fall into one of two traps: they're either heavyweight enterprise widgets with complex licensing, or they're tiny unstyled headless primitives that leave all the hard parts to you.
gridline lands in the middle. It ships a complete, production-ready grid component with every feature a real application needs, while staying small, composable, and dependency-light. The hooks are exported so you can build completely custom UIs using the same battle-tested logic underneath.
Built in the open from Stockpile, a manufacturing inventory management system that runs grids with 100k+ rows in production every day.
Features
| | |
|---|---|
| ⚡ Virtual scrolling | Handles 100 000+ rows smoothly via react-virtuoso |
| ↕️ Multi-column sort | Shift-click headers; smart comparators for numbers, dates, semver, alphanumeric codes |
| 🔍 Instant search | Debounced full-text search across all visible columns |
| 🎛️ Constraint-aware filters | Filter dropdowns that know about each other (cascading) |
| 👁️ Column visibility | Toggle columns on/off, persisted to localStorage |
| ✅ Row selection | Single or multi-select; Shift-click range select |
| ✏️ Inline editing | Double-click any cell; bring your own editor component |
| ↩️ Undo / redo | Full edit history with Ctrl+Z / Ctrl+Y |
| 📤 CSV export | Exports visible columns; respects active filters and selection |
| 💾 Persistence | Column prefs and sort state survive page reloads |
| 🎨 CSS custom properties | Style via --gl-* tokens; no CSS-in-JS required |
| 🧩 Composable hooks | Every feature is a standalone hook you can use in your own UI |
Installation
npm install @wjoffe93/gridlinePeer dependencies
npm install react react-dom react-virtuosoQuick start
import { Grid } from '@wjoffe93/gridline';
import type { ColumnDef } from '@wjoffe93/gridline';
interface Employee {
id: number;
name: string;
department: string;
salary: number;
startDate: string;
}
const columns: ColumnDef<Employee>[] = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'department', header: 'Department', sortable: true, filterable: true },
{ key: 'salary', header: 'Salary', align: 'right',
render: v => `$${Number(v).toLocaleString()}` },
{ key: 'startDate', header: 'Start date', sortable: true },
];
export default function EmployeeTable({ employees }: { employees: Employee[] }) {
return (
<Grid
data={employees}
columns={columns}
rowKey="id"
height={600}
searchable
exportable
selectable
persistKey="employees"
/>
);
}Column definitions
Every column is a ColumnDef<T> object. The only required fields are key and header.
interface ColumnDef<T> {
key: string; // Field name or dot-path ("address.city")
header: string; // Header label
// Layout
width?: number; // Fixed width in px
minWidth?: number; // Min width when resizing (default 60)
align?: 'left' | 'center' | 'right';
pin?: 'left' | 'right';
// Behaviour
sortable?: boolean; // default true
filterable?: boolean; // participates in filter panel
editable?: boolean | ((row: T) => boolean);
hidden?: boolean; // hidden by default (user can re-show)
exportable?: boolean; // included in CSV export (default true)
// Rendering
render?: (value, row, ctx) => React.ReactNode;
editor?: (props: EditorProps<T>) => React.ReactNode;
tooltip?: boolean | ((value, row) => string);
overflow?: 'ellipsis' | 'wrap' | 'clip';
// Data
getValue?: (row: T) => unknown; // computed / derived values
comparator?: (a, b, dir) => number; // custom sort logic
validate?: (value, row) => string | null;
}Grid props
<Grid
// Required
data={rows}
columns={columns}
rowKey="id" // or (row) => row.id.toString()
// Dimensions
height={500} // required for virtual scrolling
width="100%"
// Features (all false/disabled by default)
searchable
sortable // true by default
filterable
selectable // or "single" / "multi"
editable
undoable // enables Ctrl+Z for edits
exportable
resizable
reorderable
// Events
onRowClick={(row, index, event) => {}}
onSelectionChange={(selectedKeys, selectedRows) => {}}
onSortChange={(sort) => {}}
onEdit={(rowKey, colKey, next, prev) => true} // return false to reject
// Toolbar
toolbarLeading={<MyCustomButton />}
toolbarTrailing={<ColumnPicker />}
// Persistence
persistKey="my-grid" // namespaces localStorage keys
// Export
exportFilename="employees-2024"
// Initial / default state
defaultSort={[{ key: 'name', direction: 'asc' }]}
defaultHiddenColumns={['internalId']}
/>Custom cell rendering
const columns: ColumnDef<Order>[] = [
{
key: 'status',
header: 'Status',
render: (value) => (
<span className={`badge badge-${value}`}>
{String(value)}
</span>
),
},
{
key: 'revenue',
header: 'Revenue',
align: 'right',
render: (value) => new Intl.NumberFormat('en-US', {
style: 'currency', currency: 'USD',
}).format(Number(value)),
comparator: (a, b, dir) => {
const diff = Number(a) - Number(b);
return dir === 'asc' ? diff : -diff;
},
},
];Inline editing
import type { EditorProps, ColumnDef } from '@wjoffe93/gridline';
function SelectEditor<T>({ value, onCommit, onCancel }: EditorProps<T>) {
return (
<select
autoFocus
defaultValue={String(value)}
onChange={e => onCommit(e.target.value)}
onKeyDown={e => e.key === 'Escape' && onCancel()}
>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
);
}
const col: ColumnDef<User> = {
key: 'status',
header: 'Status',
editable: true,
editor: SelectEditor,
validate: (v) => ['active', 'inactive'].includes(String(v)) ? null : 'Invalid status',
};Enable editing on the grid and optionally hook into the commit event:
<Grid
editable
undoable
onEdit={async (rowKey, colKey, next, prev) => {
const ok = await api.updateField(rowKey, colKey, next);
return ok; // return false to roll back the edit
}}
/>Using hooks directly
Every feature is a standalone hook. Use them to build completely custom table UIs:
import {
useSort, useFilter, useSearch,
useColumnVisibility, useRowSelection,
sortRows, filterRows, searchRows,
} from '@wjoffe93/gridline';
function MyCustomTable({ data, columns }) {
const { sort, onHeaderClick, getSortDir } = useSort({ persistKey: 'my-table' });
const { filters, setFilter } = useFilter({});
const { debouncedValue: search, setSearch } = useSearch({});
const { visibleColumns, toggle } = useColumnVisibility(columns);
const { selectedKeys, toggle: select } = useRowSelection(data, r => r.id);
const rows = searchRows(filterRows(sortRows(data, sort, columns), filters, columns), search, columns);
return (
<div>
<input onChange={e => setSearch(e.target.value)} placeholder="Search…" />
{/* your own table JSX here */}
</div>
);
}Theming
gridline uses CSS custom properties. Override them to match your design system:
.my-grid {
--gl-row-height: 40px;
--gl-header-height: 36px;
--gl-border: #e2e8f0;
--gl-header-bg: #f8fafc;
--gl-header-color: #475569;
--gl-selected-bg: #dbeafe;
--gl-cell-x: 14px; /* horizontal cell padding */
}Docs
Contributing
See CONTRIBUTING.md. PRs welcome — please open an issue first for large changes.
