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

@mayur.sarvadhi/virtual-grid

v1.0.1

Published

A high-performance, feature-rich virtualized grid component for React with sorting, filtering, cell selection, row selection, column resize, and more.

Readme

VirtualGrid

A high-performance, feature-rich virtualized grid component for React. Built to handle millions of rows with smooth scrolling, sorting, filtering, cell selection, row selection, column resizing, and more.

Features

Core Features

  • 🚀 Virtual Scrolling - Only renders visible rows for optimal performance
  • 📊 Large Dataset Support - Handles 1M+ rows smoothly
  • 🔄 Sorting - Client-side sorting with custom sorters
  • 🔍 Filtering - Column-based filtering with searchable dropdowns
  • 📝 Cell Selection - Excel-like cell selection with keyboard navigation
  • ☑️ Row Selection - Single and multi-row selection with checkboxes
  • 📏 Column Resize - Drag to resize columns
  • 🔀 Column Reorder - Drag and drop columns to reorder
  • ✏️ Inline Editing - Edit cells directly in the grid
  • 🎨 Theming - Fully customizable themes
  • 📱 Responsive - Mobile-friendly design
  • ⌨️ Keyboard Navigation - Full keyboard support for cell selection
  • 📋 Copy to Clipboard - Copy selected cells (Ctrl+C / Cmd+C)

Installation

npm install @mayur.sarvadhi/virtual-grid
# or
yarn add @mayur.sarvadhi/virtual-grid
# or
pnpm add @mayur.sarvadhi/virtual-grid

Peer Dependencies

This package requires React 18 or 19:

npm install react react-dom

Quick Start

import React from "react";
import VirtualGrid, { Column } from "@mayur.sarvadhi/virtual-grid";
import "@mayur.sarvadhi/virtual-grid/dist/VirtualGrid.css";

const App = () => {
  const data = [
    { id: 1, name: "John Doe", email: "[email protected]", age: 30 },
    { id: 2, name: "Jane Smith", email: "[email protected]", age: 25 },
    // ... more data
  ];

  const columns: Column[] = [
    { key: "id", dataIndex: "id", title: "ID", width: 80 },
    { key: "name", dataIndex: "name", title: "Name", width: 150 },
    { key: "email", dataIndex: "email", title: "Email", width: 220 },
    { key: "age", dataIndex: "age", title: "Age", width: 80 },
  ];

  return (
    <div style={{ height: "600px" }}>
      <VirtualGrid dataSource={data} columns={columns} />
    </div>
  );
};

API Reference

VirtualGrid Props

| Prop | Type | Default | Description | | ---------------------- | -------------------------------------------------- | ------------ | --------------------------------------------- | | dataSource | Record<string, unknown>[] | Required | Array of data objects to display | | originalDataSource | Record<string, unknown>[] | undefined | Original unfiltered data (for filtering) | | columns | Column[] | Required | Column configuration array | | rowHeight | number | 30 | Height of each row in pixels | | headerHeight | number | 30 | Height of header row in pixels | | overscan | number | 3 | Number of rows to render outside visible area | | style | React.CSSProperties | undefined | Custom styles for the grid container | | className | string | '' | Additional CSS class name | | rowClassName | (record, index) => string | undefined | Function to generate row class names | | sortState | SortState | undefined | Controlled sort state | | onSort | (columnKey, direction) => void | undefined | Callback when column is sorted | | enableCellSelection | boolean | false | Enable Excel-like cell selection | | onCellSelection | (selectedCells) => void | undefined | Callback when cell selection changes | | enableRowSelection | boolean | false | Enable row selection with checkboxes | | rowKey | string | undefined | Unique key field in data (defaults to index) | | selectedRowKeys | Array<string \| number> | undefined | Controlled selected row keys | | onRowSelectionChange | (keys, rows) => void | undefined | Callback when row selection changes | | enableColumnResize | boolean | false | Enable column resizing | | onColumnResize | (columnKey, newWidth) => void | undefined | Callback when column is resized | | columnWidths | Record<string, number> | {} | Controlled column widths | | enableColumnReorder | boolean | false | Enable column reordering | | onColumnReorder | (sourceIndex, targetIndex) => void | undefined | Callback when column is reordered | | filters | Record<string, Set<string \| number \| boolean>> | undefined | Controlled filter state | | onFiltersChange | (filters) => void | undefined | Callback when filters change | | onCellEdit | (params) => void | undefined | Callback when cell is edited | | onRowClick | (record, index) => void | undefined | Callback when row is clicked | | theme | Theme | {} | Theme configuration object |

Column Interface

interface Column {
  key: string; // Unique column identifier
  dataIndex: string; // Field name in data object
  title: string; // Column header text
  width?: number; // Column width (default: 150)
  minWidth?: number; // Minimum column width
  align?: "left" | "center" | "right"; // Text alignment
  sortable?: boolean; // Enable sorting
  sorter?: (a, b) => number; // Custom sort function
  filterable?: boolean; // Enable filtering
  editable?: boolean; // Enable inline editing
  editor?: EditorConfig; // Editor configuration
  render?: (value, record, index) => React.ReactNode; // Custom cell renderer
}

Editor Configuration

// Text input
editor: { type: 'text' }

// Number input
editor: { type: 'number' }

// Date picker
editor: { type: 'date' }

// Checkbox
editor: { type: 'checkbox' }

// Select dropdown
editor: {
  type: 'select',
  options: [
    { label: 'Option 1', value: 'value1' },
    { label: 'Option 2', value: 'value2' }
  ]
}

Theme Interface

interface Theme {
  headerBackground?: string; // Header background color
  headerColor?: string; // Header text color
  rowBackground?: string; // Row background color
  rowAlternateBackground?: string; // Alternating row background
  rowHoverBackground?: string; // Row hover background
  borderColor?: string; // Border color
  fontSize?: string; // Font size
  fontFamily?: string; // Font family
  rowColor?: string; // Row text color
}

Callbacks Reference

onSort(columnKey: string, direction: 'asc' | 'desc' | null) => void

Called when a sortable column header is clicked.

Parameters:

  • columnKey: The key of the column being sorted
  • direction: Sort direction ('asc', 'desc', or null to clear)

Example:

const [sortState, setSortState] = useState<SortState>({
  columnKey: null,
  direction: null,
});

const handleSort = (columnKey: string, direction: "asc" | "desc" | null) => {
  setSortState({ columnKey, direction });
  // Perform sorting logic here
};

<VirtualGrid
  sortState={sortState}
  onSort={handleSort}
  // ... other props
/>;

onRowSelectionChange(selectedRowKeys: Array<string | number>, selectedRows: Record<string, unknown>[]) => void

Called when row selection changes (checkbox clicked or programmatically).

Parameters:

  • selectedRowKeys: Array of selected row keys
  • selectedRows: Array of selected row data objects

Example:

const [selectedKeys, setSelectedKeys] = useState<Array<string | number>>([]);

const handleSelectionChange = (
  keys: Array<string | number>,
  rows: Record<string, unknown>[]
) => {
  setSelectedKeys(keys);
  console.log("Selected:", rows);
};

<VirtualGrid
  enableRowSelection
  rowKey="id"
  selectedRowKeys={selectedKeys}
  onRowSelectionChange={handleSelectionChange}
  // ... other props
/>;

onCellSelection(selectedCells: CellSelection[]) => void

Called when cell selection changes (click, drag, or keyboard navigation).

Parameters:

  • selectedCells: Array of selected cells with structure:
    {
      rowIndex: number;
      columnKey: string;
      value: unknown;
    }
    [];

Example:

const handleCellSelection = (cells: CellSelection[]) => {
  console.log("Selected cells:", cells);
  // Copy to clipboard, export, etc.
};

<VirtualGrid
  enableCellSelection
  onCellSelection={handleCellSelection}
  // ... other props
/>;

Keyboard Shortcuts:

  • Arrow Keys: Navigate between cells
  • Shift + Arrow Keys: Select range
  • Ctrl/Cmd + C: Copy selected cells to clipboard
  • Home/End: Jump to first/last column
  • Page Up/Down: Jump by 10 rows

onColumnResize(columnKey: string, newWidth: number) => void

Called when a column is resized by dragging.

Parameters:

  • columnKey: The key of the resized column
  • newWidth: New width in pixels

Example:

const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});

const handleResize = (columnKey: string, newWidth: number) => {
  setColumnWidths((prev) => ({ ...prev, [columnKey]: newWidth }));
};

<VirtualGrid
  enableColumnResize
  columnWidths={columnWidths}
  onColumnResize={handleResize}
  // ... other props
/>;

onColumnReorder(sourceIndex: number, targetIndex: number) => void

Called when a column is dragged to a new position.

Parameters:

  • sourceIndex: Original column index
  • targetIndex: New column index

Example:

const [columns, setColumns] = useState<Column[]>(initialColumns);

const handleReorder = (sourceIndex: number, targetIndex: number) => {
  const newColumns = [...columns];
  const [removed] = newColumns.splice(sourceIndex, 1);
  newColumns.splice(targetIndex, 0, removed);
  setColumns(newColumns);
};

<VirtualGrid
  enableColumnReorder
  columns={columns}
  onColumnReorder={handleReorder}
  // ... other props
/>;

onFiltersChange(filters: Record<string, Set<string | number | boolean>>) => void

Called when column filters are applied or cleared.

Parameters:

  • filters: Object mapping column keys to sets of selected filter values

Example:

const [filters, setFilters] = useState<
  Record<string, Set<string | number | boolean>>
>({});

const handleFiltersChange = (
  nextFilters: Record<string, Set<string | number | boolean>>
) => {
  setFilters(nextFilters);
  // Apply filters to dataSource
};

<VirtualGrid
  filters={filters}
  onFiltersChange={handleFiltersChange}
  // ... other props
/>;

onCellEdit(params: { rowIndex: number, rowKeyValue: string | number, columnKey: string, dataIndex: string, value: unknown }) => void

Called when an editable cell value is changed.

Parameters:

  • rowIndex: Index of the edited row
  • rowKeyValue: Unique key value of the row
  • columnKey: Key of the edited column
  • dataIndex: Data field name
  • value: New cell value

Example:

const handleCellEdit = (params: {
  rowIndex: number;
  rowKeyValue: string | number;
  columnKey: string;
  dataIndex: string;
  value: unknown;
}) => {
  // Update your data source
  const updatedData = [...dataSource];
  updatedData[params.rowIndex][params.dataIndex] = params.value;
  setDataSource(updatedData);
};

<VirtualGrid
  onCellEdit={handleCellEdit}
  // ... other props
/>;

onRowClick(record: Record<string, unknown>, index: number) => void

Called when a row is clicked.

Parameters:

  • record: The row data object
  • index: Row index

Example:

const handleRowClick = (record: Record<string, unknown>, index: number) => {
  console.log("Clicked row:", record);
  // Navigate to detail page, show modal, etc.
};

<VirtualGrid
  onRowClick={handleRowClick}
  // ... other props
/>;

Usage Examples

Basic Grid

import VirtualGrid, { Column } from "@mayur.sarvadhi/virtual-grid";
import "@mayur.sarvadhi/virtual-grid/dist/VirtualGrid.css";

const data = [
  { id: 1, name: "John", age: 30 },
  { id: 2, name: "Jane", age: 25 },
];

const columns: Column[] = [
  { key: "id", dataIndex: "id", title: "ID", width: 80 },
  { key: "name", dataIndex: "name", title: "Name", width: 150 },
  { key: "age", dataIndex: "age", title: "Age", width: 80 },
];

<VirtualGrid dataSource={data} columns={columns} />;

Sorting

const [sortState, setSortState] = useState<SortState>({
  columnKey: null,
  direction: null,
});

const [sortedData, setSortedData] = useState(data);

const handleSort = (columnKey: string, direction: "asc" | "desc" | null) => {
  setSortState({ columnKey, direction });

  if (!direction) {
    setSortedData(data);
    return;
  }

  const sorted = [...data].sort((a, b) => {
    const aVal = a[columnKey];
    const bVal = b[columnKey];

    if (direction === "asc") {
      return aVal > bVal ? 1 : -1;
    } else {
      return aVal < bVal ? 1 : -1;
    }
  });

  setSortedData(sorted);
};

const columns: Column[] = [
  {
    key: "name",
    dataIndex: "name",
    title: "Name",
    width: 150,
    sortable: true,
  },
  // ... more columns
];

<VirtualGrid
  dataSource={sortedData}
  columns={columns}
  sortState={sortState}
  onSort={handleSort}
/>;

Filtering

const [filters, setFilters] = useState<
  Record<string, Set<string | number | boolean>>
>({});
const [filteredData, setFilteredData] = useState(data);

const handleFiltersChange = (
  nextFilters: Record<string, Set<string | number | boolean>>
) => {
  setFilters(nextFilters);

  let filtered = [...data];

  Object.entries(nextFilters).forEach(([columnKey, filterSet]) => {
    if (filterSet.size > 0) {
      filtered = filtered.filter((row) => filterSet.has(row[columnKey]));
    }
  });

  setFilteredData(filtered);
};

const columns: Column[] = [
  {
    key: "department",
    dataIndex: "department",
    title: "Department",
    width: 150,
    filterable: true,
  },
  // ... more columns
];

<VirtualGrid
  dataSource={filteredData}
  originalDataSource={data}
  columns={columns}
  filters={filters}
  onFiltersChange={handleFiltersChange}
/>;

Row Selection

const [selectedKeys, setSelectedKeys] = useState<Array<string | number>>([]);

const handleSelectionChange = (
  keys: Array<string | number>,
  rows: Record<string, unknown>[]
) => {
  setSelectedKeys(keys);
  console.log(`${keys.length} rows selected`);
};

<VirtualGrid
  dataSource={data}
  columns={columns}
  enableRowSelection
  rowKey="id"
  selectedRowKeys={selectedKeys}
  onRowSelectionChange={handleSelectionChange}
/>;

Cell Selection

const handleCellSelection = (cells: CellSelection[]) => {
  console.log("Selected cells:", cells);
};

<VirtualGrid
  dataSource={data}
  columns={columns}
  enableCellSelection
  onCellSelection={handleCellSelection}
/>;

Inline Editing

const [dataSource, setDataSource] = useState(data);

const handleCellEdit = (params: {
  rowIndex: number;
  rowKeyValue: string | number;
  columnKey: string;
  dataIndex: string;
  value: unknown;
}) => {
  const updated = [...dataSource];
  updated[params.rowIndex][params.dataIndex] = params.value;
  setDataSource(updated);
};

const columns: Column[] = [
  {
    key: "name",
    dataIndex: "name",
    title: "Name",
    width: 150,
    editable: true,
    editor: { type: "text" },
  },
  {
    key: "age",
    dataIndex: "age",
    title: "Age",
    width: 80,
    editable: true,
    editor: { type: "number" },
  },
  {
    key: "status",
    dataIndex: "status",
    title: "Status",
    width: 120,
    editable: true,
    editor: {
      type: "select",
      options: [
        { label: "Active", value: "active" },
        { label: "Inactive", value: "inactive" },
      ],
    },
  },
];

<VirtualGrid
  dataSource={dataSource}
  columns={columns}
  onCellEdit={handleCellEdit}
/>;

Column Resize

const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});

const handleResize = (columnKey: string, newWidth: number) => {
  setColumnWidths((prev) => ({
    ...prev,
    [columnKey]: newWidth,
  }));
};

<VirtualGrid
  dataSource={data}
  columns={columns}
  enableColumnResize
  columnWidths={columnWidths}
  onColumnResize={handleResize}
/>;

Column Reorder

const [columns, setColumns] = useState<Column[]>(initialColumns);

const handleReorder = (sourceIndex: number, targetIndex: number) => {
  const newColumns = [...columns];
  const [removed] = newColumns.splice(sourceIndex, 1);
  newColumns.splice(targetIndex, 0, removed);
  setColumns(newColumns);
};

<VirtualGrid
  dataSource={data}
  columns={columns}
  enableColumnReorder
  onColumnReorder={handleReorder}
/>;

Custom Cell Rendering

const columns: Column[] = [
  {
    key: "status",
    dataIndex: "status",
    title: "Status",
    width: 120,
    render: (value) => (
      <span
        style={{
          padding: "4px 8px",
          borderRadius: "4px",
          background: value === "active" ? "#52c41a" : "#ff4d4f",
          color: "white",
        }}
      >
        {value}
      </span>
    ),
  },
  {
    key: "avatar",
    dataIndex: "avatar",
    title: "Avatar",
    width: 80,
    render: (value, record) => (
      <img
        src={value}
        alt={record.name}
        style={{ width: 40, height: 40, borderRadius: "50%" }}
      />
    ),
  },
];

Theming

const darkTheme = {
  headerBackground: "#1a1a1a",
  headerColor: "#ffffff",
  rowBackground: "#141414",
  rowAlternateBackground: "#1f1f1f",
  rowHoverBackground: "#262626",
  borderColor: "#303030",
  fontSize: "14px",
  fontFamily: "system-ui, sans-serif",
  rowColor: "#ffffff",
};

<VirtualGrid dataSource={data} columns={columns} theme={darkTheme} />;

Combining Multiple Features

<VirtualGrid
  dataSource={data}
  columns={columns}
  enableRowSelection
  enableCellSelection
  enableColumnResize
  enableColumnReorder
  rowKey="id"
  sortState={sortState}
  onSort={handleSort}
  filters={filters}
  onFiltersChange={handleFiltersChange}
  selectedRowKeys={selectedKeys}
  onRowSelectionChange={handleSelectionChange}
  columnWidths={columnWidths}
  onColumnResize={handleResize}
  onColumnReorder={handleReorder}
  onCellEdit={handleCellEdit}
  theme={customTheme}
/>

Performance Tips

  1. Use rowKey for stable row identification
  2. Memoize columns if they're computed
  3. Use originalDataSource for filtering to show all possible filter values
  4. Avoid unnecessary re-renders by memoizing callbacks
  5. Adjust overscan based on your needs (lower = faster, higher = smoother)

Browser Support

  • Chrome (latest)
  • Firefox (latest)
  • Safari (latest)
  • Edge (latest)

TypeScript

This package is written in TypeScript and includes full type definitions. No additional @types package needed.

Publishing to NPM

Before publishing, make sure to:

  1. Update package name in package.json (set to @mayur.sarvadhi/virtual-grid using your npm username)
  2. Update repository URL in package.json if you have a GitHub repository
  3. Update author field in package.json
  4. Build the package:
    npm run build
  5. Test the build locally if needed

Publishing as Private First (Then Make Public Later)

If you want to publish the package privately first and make it public later:

Step 1: Publish as Private/Restricted

For scoped packages (packages starting with @), you can publish with restricted access:

npm login
npm publish --access restricted

What this does:

  • Package is published to npm registry
  • Only visible/searchable by you and users you grant access to
  • Requires npm paid plan for truly private packages (free tier allows restricted access for scoped packages)
  • Others cannot install it without your permission

Alternative: Keep Package Local (Not Published Yet)

If you want to keep it completely private and not publish yet, add this to your package.json:

{
  "private": true,
  ...
}

This prevents accidental publishing. Remove it when ready to publish.

Step 2: Make It Public Later

When you're ready to make your package public, you have two options:

Option A: Change access of existing package

npm access public @mayur.sarvadhi/virtual-grid

Option B: Publish new version as public

npm publish --access public

Publishing Directly as Public

If you want to publish directly as public:

npm login
npm publish --access public

Note: Once published as public, anyone can see and install your package. You can always unpublish within 72 hours, but it's better to start private if you're unsure.

Recommended Workflow

  1. Development Phase:

    • Add "private": true to package.json OR use --access restricted
    • Test and iterate on the package
    • Build and test locally
  2. Private Publishing Phase:

    • Remove "private": true if you added it
    • Publish with: npm publish --access restricted
    • Share with specific team members if needed
  3. Going Public:

    • When ready: npm access public @mayur.sarvadhi/virtual-grid
    • Or publish next version: npm publish --access public

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Support

For issues, questions, or contributions, please open an issue on GitHub.