A high-performance, feature-rich virtualized grid component for React with sorting, filtering, cell selection, row selection, column resize, and more.
npm install @mayur.sarvadhi/virtual-gridA 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.
⨠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)
``bash`
npm install @mayur.sarvadhi/virtual-gridor
yarn add @mayur.sarvadhi/virtual-gridor
pnpm add @mayur.sarvadhi/virtual-grid
This package requires React 18 or 19:
`bash`
npm install react react-dom
`tsx
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: "john@example.com", age: 30 },
{ id: 2, name: "Jane Smith", email: "jane@example.com", 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 (
API Reference
$3
| Prop | Type | Default | Description |
| ---------------------- | -------------------------------------------------- | ------------ | --------------------------------------------- |
|
dataSource | Record | Required | Array of data objects to display |
| originalDataSource | Record | 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 | 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 | {} | Controlled column widths |
| enableColumnReorder | boolean | false | Enable column reordering |
| onColumnReorder | (sourceIndex, targetIndex) => void | undefined | Callback when column is reordered |
| filters | Record | 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 |$3
`typescript
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
}
`$3
`typescript
// 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' }
]
}
`$3
`typescript
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
$3
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:
`tsx
const [sortState, setSortState] = useState({
columnKey: null,
direction: null,
});const handleSort = (columnKey: string, direction: "asc" | "desc" | null) => {
setSortState({ columnKey, direction });
// Perform sorting logic here
};
sortState={sortState}
onSort={handleSort}
// ... other props
/>;
`$3
Called when row selection changes (checkbox clicked or programmatically).
Parameters:
-
selectedRowKeys: Array of selected row keys
- selectedRows: Array of selected row data objectsExample:
`tsx
const [selectedKeys, setSelectedKeys] = useState>([]);const handleSelectionChange = (
keys: Array,
rows: Record[]
) => {
setSelectedKeys(keys);
console.log("Selected:", rows);
};
enableRowSelection
rowKey="id"
selectedRowKeys={selectedKeys}
onRowSelectionChange={handleSelectionChange}
// ... other props
/>;
`$3
Called when cell selection changes (click, drag, or keyboard navigation).
Parameters:
-
selectedCells: Array of selected cells with structure:
`typescript
{
rowIndex: number;
columnKey: string;
value: unknown;
}
[];
`Example:
`tsx
const handleCellSelection = (cells: CellSelection[]) => {
console.log("Selected cells:", cells);
// Copy to clipboard, export, etc.
}; 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$3
Called when a column is resized by dragging.
Parameters:
-
columnKey: The key of the resized column
- newWidth: New width in pixelsExample:
`tsx
const [columnWidths, setColumnWidths] = useState>({});const handleResize = (columnKey: string, newWidth: number) => {
setColumnWidths((prev) => ({ ...prev, [columnKey]: newWidth }));
};
enableColumnResize
columnWidths={columnWidths}
onColumnResize={handleResize}
// ... other props
/>;
`$3
Called when a column is dragged to a new position.
Parameters:
-
sourceIndex: Original column index
- targetIndex: New column indexExample:
`tsx
const [columns, setColumns] = useState(initialColumns);const handleReorder = (sourceIndex: number, targetIndex: number) => {
const newColumns = [...columns];
const [removed] = newColumns.splice(sourceIndex, 1);
newColumns.splice(targetIndex, 0, removed);
setColumns(newColumns);
};
enableColumnReorder
columns={columns}
onColumnReorder={handleReorder}
// ... other props
/>;
`$3
Called when column filters are applied or cleared.
Parameters:
-
filters: Object mapping column keys to sets of selected filter valuesExample:
`tsx
const [filters, setFilters] = useState<
Record>
>({});const handleFiltersChange = (
nextFilters: Record>
) => {
setFilters(nextFilters);
// Apply filters to dataSource
};
filters={filters}
onFiltersChange={handleFiltersChange}
// ... other props
/>;
`$3
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 valueExample:
`tsx
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);
}; onCellEdit={handleCellEdit}
// ... other props
/>;
`$3
Called when a row is clicked.
Parameters:
-
record: The row data object
- index: Row indexExample:
`tsx
const handleRowClick = (record: Record, index: number) => {
console.log("Clicked row:", record);
// Navigate to detail page, show modal, etc.
}; onRowClick={handleRowClick}
// ... other props
/>;
`Usage Examples
$3
`tsx
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 },
];
;
`$3
`tsx
const [sortState, setSortState] = useState({
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
];
dataSource={sortedData}
columns={columns}
sortState={sortState}
onSort={handleSort}
/>;
`$3
`tsx
const [filters, setFilters] = useState<
Record>
>({});
const [filteredData, setFilteredData] = useState(data);const handleFiltersChange = (
nextFilters: Record>
) => {
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
];
dataSource={filteredData}
originalDataSource={data}
columns={columns}
filters={filters}
onFiltersChange={handleFiltersChange}
/>;
`$3
`tsx
const [selectedKeys, setSelectedKeys] = useState>([]);const handleSelectionChange = (
keys: Array,
rows: Record[]
) => {
setSelectedKeys(keys);
console.log(
${keys.length} rows selected);
}; dataSource={data}
columns={columns}
enableRowSelection
rowKey="id"
selectedRowKeys={selectedKeys}
onRowSelectionChange={handleSelectionChange}
/>;
`$3
`tsx
const handleCellSelection = (cells: CellSelection[]) => {
console.log("Selected cells:", cells);
}; dataSource={data}
columns={columns}
enableCellSelection
onCellSelection={handleCellSelection}
/>;
`$3
`tsx
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" },
],
},
},
];
dataSource={dataSource}
columns={columns}
onCellEdit={handleCellEdit}
/>;
`$3
`tsx
const [columnWidths, setColumnWidths] = useState>({});const handleResize = (columnKey: string, newWidth: number) => {
setColumnWidths((prev) => ({
...prev,
[columnKey]: newWidth,
}));
};
dataSource={data}
columns={columns}
enableColumnResize
columnWidths={columnWidths}
onColumnResize={handleResize}
/>;
`$3
`tsx
const [columns, setColumns] = useState(initialColumns);const handleReorder = (sourceIndex: number, targetIndex: number) => {
const newColumns = [...columns];
const [removed] = newColumns.splice(sourceIndex, 1);
newColumns.splice(targetIndex, 0, removed);
setColumns(newColumns);
};
dataSource={data}
columns={columns}
enableColumnReorder
onColumnReorder={handleReorder}
/>;
`$3
`tsx
const columns: Column[] = [
{
key: "status",
dataIndex: "status",
title: "Status",
width: 120,
render: (value) => (
style={{
padding: "4px 8px",
borderRadius: "4px",
background: value === "active" ? "#52c41a" : "#ff4d4f",
color: "white",
}}
>
{value}
),
},
{
key: "avatar",
dataIndex: "avatar",
title: "Avatar",
width: 80,
render: (value, record) => (
src={value}
alt={record.name}
style={{ width: 40, height: 40, borderRadius: "50%" }}
/>
),
},
];
`$3
`tsx
const darkTheme = {
headerBackground: "#1a1a1a",
headerColor: "#ffffff",
rowBackground: "#141414",
rowAlternateBackground: "#1f1f1f",
rowHoverBackground: "#262626",
borderColor: "#303030",
fontSize: "14px",
fontFamily: "system-ui, sans-serif",
rowColor: "#ffffff",
}; ;
`$3
`tsx
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:
`bash
npm run build
`
5. Test the build locally if needed$3
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:`bash
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:`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
`bash
npm access public @mayur.sarvadhi/virtual-grid
`Option B: Publish new version as public
`bash
npm publish --access public
`$3
If you want to publish directly as public:
`bash
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.
$3
1. Development Phase:
- Add
"private": true to package.json OR use --access restricted
- Test and iterate on the package
- Build and test locally2. Private Publishing Phase:
- Remove
"private": true if you added it
- Publish with: npm publish --access restricted
- Share with specific team members if needed3. Going Public:
- When ready:
npm access public @mayur.sarvadhi/virtual-grid
- Or publish next version: npm publish --access public`MIT
Contributions are welcome! Please feel free to submit a Pull Request.
For issues, questions, or contributions, please open an issue on GitHub.