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

@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.

Readme

gridline

A fast, virtualized data grid for React — sorting, filtering, inline editing, undo/redo, and CSV export without a framework tax.

CI npm version bundle size license


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/gridline

Peer dependencies

npm install react react-dom react-virtuoso

Quick 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.


License

MIT