@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.
Maintainers
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-gridPeer Dependencies
This package requires React 18 or 19:
npm install react react-domQuick 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 sorteddirection: Sort direction ('asc','desc', ornullto 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 keysselectedRows: 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 cellsShift + Arrow Keys: Select rangeCtrl/Cmd + C: Copy selected cells to clipboardHome/End: Jump to first/last columnPage 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 columnnewWidth: 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 indextargetIndex: 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 rowrowKeyValue: Unique key value of the rowcolumnKey: Key of the edited columndataIndex: Data field namevalue: 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 objectindex: 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
- Use
rowKeyfor stable row identification - Memoize columns if they're computed
- Use
originalDataSourcefor filtering to show all possible filter values - Avoid unnecessary re-renders by memoizing callbacks
- Adjust
overscanbased 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:
- Update package name in
package.json(set to@mayur.sarvadhi/virtual-gridusing your npm username) - Update repository URL in
package.jsonif you have a GitHub repository - Update author field in
package.json - Build the package:
npm run build - 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 restrictedWhat 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-gridOption B: Publish new version as public
npm publish --access publicPublishing Directly as Public
If you want to publish directly as public:
npm login
npm publish --access publicNote: 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
Development Phase:
- Add
"private": truetopackage.jsonOR use--access restricted - Test and iterate on the package
- Build and test locally
- Add
Private Publishing Phase:
- Remove
"private": trueif you added it - Publish with:
npm publish --access restricted - Share with specific team members if needed
- Remove
Going Public:
- When ready:
npm access public @mayur.sarvadhi/virtual-grid - Or publish next version:
npm publish --access public
- When ready:
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.
