A declarative, high-performance virtual table component for React with automatic row virtualization.
npm install simple-virtual-table-reactA declarative, high-performance virtual table component for React with automatic row virtualization.
``bash`
npm install simple-virtual-table-reactor
yarn add simple-virtual-table-reactor
pnpm add simple-virtual-table-react
`tsx
import { Table, Tbody, Td, Th, Thead, Tr } from "simple-virtual-table-react";
interface User {
id: number;
name: string;
email: string;
age: number;
status: string;
width?: number;
phone?: string;
department?: string;
salary?: number;
location?: string;
joinDate?: string;
manager?: string;
}
const colors: Record
Active: "#51cf66",
Inactive: "#ffd43b",
Pending: "#74c0fc",
};
const generateData = (count: number): User[] => {
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: User ${i + 1},user${i + 1}@example.com
email: ,
age: 20 + (i % 50),
status: i % 3 === 0 ? "Active" : i % 3 === 1 ? "Inactive" : "Pending",
}));
};
const data = generateData(10000); // Large dataset to showcase virtualization
const smallData: User[] = [
{
id: 1,
name: "John Doe",
email: "john@example.com",
age: 30,
status: "Active",
width: 100,
phone: "+1-555-0101",
department: "Engineering",
salary: 95000,
location: "New York",
joinDate: "2020-01-15",
manager: "Alice Johnson",
},
{
id: 2,
name: "Jane Smith",
email: "jane@example.com",
age: 25,
status: "Inactive",
width: 200,
phone: "+1-555-0102",
department: "Marketing",
salary: 72000,
location: "San Francisco",
joinDate: "2021-03-22",
manager: "Bob Williams",
},
{
id: 3,
name: "Jim Johnson",
email: "jim@example.com",
age: 35,
status: "Pending",
width: 150,
phone: "+1-555-0103",
department: "Sales",
salary: 85000,
location: "Chicago",
joinDate: "2019-06-10",
manager: "Carol Davis",
},
{
id: 4,
name: "Jill Williams",
email: "jill@example.com",
age: 40,
status: "Active",
width: 180,
phone: "+1-555-0104",
department: "HR",
salary: 68000,
location: "Boston",
joinDate: "2022-02-08",
manager: "David Brown",
},
{
id: 5,
name: "Jack Brown",
email: "jack@example.com",
age: 45,
status: "Inactive",
width: 120,
phone: "+1-555-0105",
department: "Engineering",
salary: 110000,
location: "Seattle",
joinDate: "2018-09-12",
manager: "Alice Johnson",
},
{
id: 6,
name: "Sarah Davis",
email: "sarah@example.com",
age: 28,
status: "Pending",
width: 160,
phone: "+1-555-0106",
department: "Marketing",
salary: 75000,
location: "Los Angeles",
joinDate: "2021-11-05",
manager: "Bob Williams",
},
{
id: 7,
name: "Mike Wilson",
email: "mike@example.com",
age: 32,
status: "Active",
width: 140,
phone: "+1-555-0107",
department: "Sales",
salary: 88000,
location: "Austin",
joinDate: "2020-07-20",
manager: "Carol Davis",
},
{
id: 8,
name: "Emily Taylor",
email: "emily@example.com",
age: 27,
status: "Inactive",
width: 170,
phone: "+1-555-0108",
department: "Finance",
salary: 92000,
location: "Denver",
joinDate: "2022-04-14",
manager: "Frank Miller",
},
{
id: 9,
name: "David Anderson",
email: "david@example.com",
age: 38,
status: "Pending",
width: 110,
phone: "+1-555-0109",
department: "Engineering",
salary: 105000,
location: "Portland",
joinDate: "2019-12-03",
manager: "Alice Johnson",
},
{
id: 10,
name: "Lisa Martinez",
email: "lisa@example.com",
age: 33,
status: "Active",
width: 190,
phone: "+1-555-0110",
department: "HR",
salary: 70000,
location: "Miami",
joinDate: "2021-08-18",
manager: "David Brown",
},
{
id: 11,
name: "Tom Thompson",
email: "tom@example.com",
age: 29,
status: "Inactive",
width: 130,
phone: "+1-555-0111",
department: "Finance",
salary: 78000,
location: "Phoenix",
joinDate: "2022-01-25",
manager: "Frank Miller",
},
{
id: 12,
name: "Anna Garcia",
email: "anna@example.com",
age: 36,
status: "Pending",
width: 145,
phone: "+1-555-0112",
department: "Sales",
salary: 82000,
location: "Nashville",
joinDate: "2020-05-30",
manager: "Carol Davis",
},
];
function App() {
return (
Showing {data.length.toLocaleString()} rows with virtual scrolling
| ID | Name | Age | Status | |
|---|---|---|---|---|
{_row.id} | {_row.name} | {_row.email} | style={{ color: _row.age > 50 ? "#ff6b6b" : "#51cf66", }} > {_row.age} | style={{ padding: "4px 8px", borderRadius: "4px", backgroundColor: colors[status] + "20", color: colors[status], fontSize: "12px", fontWeight: 500, }} > {status} |
| ID | Name | Age | Status | Phone | Department | Salary | Location | Join Date | Manager | |
|---|---|---|---|---|---|---|---|---|---|---|
| {row.id} | {row.name} | {row.email} | {row.age} | {row.status} | {row.phone || "-"} | {row.department || "-"} | {row.salary ? $${row.salary.toLocaleString()} : "-"} | {row.location || "-"} | {row.joinDate || "-"} | {row.manager || "-"} |
export default App;
`
The root component that provides context to all child components.
Props:
| Prop | Type | Required | Default | Description |
| -------------------- | --------------------- | -------- | ------- | --------------------------------------------- |
| totalData | number | Yes | - | Total number of rows in the dataset |height
| | number | No | 200 | Height of the table container |rowHeight
| | number | No | 40 | Height of each row in pixels |overscan
| | number | No | 5 | Number of rows to render outside visible area |containerStyle
| | React.CSSProperties | No | - | Custom styles for the table container |containerClassName
| | string | No | - | CSS classname |children
| | ReactNode | Yes | - | Child components (Thead, Tbody) |
Header container component. Must wrap all Th components.
Props:
| Prop | Type | Required | Default | Description |
| -------------- | --------------------- | -------- | ------- | --------------------------------------------------------------------------------- |
| headerHeight | number | No | 50 | Height of the header row |style
| | React.CSSProperties | No | - | Custom styles for the header container |...props
| | HTMLDivElement | No | - | All standard HTML div attributes (className, onClick, onMouseOver, data-\*, etc.) |
Note:
- Thead automatically collects column widths from Th children and updates the table context.Thead
- automatically injects colIndex prop to all Th children.headerHeight
- The prop is optional and defaults to 50px. Typically you can omit this prop and use the default.
- All standard HTML div attributes are accepted and will be applied to the header container element.
Header cell component. Must be used inside Thead.
Props:
| Prop | Type | Required | Default | Description |
| ---------- | --------------------- | -------- | ------- | --------------------------------------------------------------------------------- |
| width | number | No | 100 | Width of the column in pixels |colIndex
| | number | No | - | Automatically injected by Thead |style
| | React.CSSProperties | No | - | Custom styles for the header cell |...props
| | HTMLDivElement | No | - | All standard HTML div attributes (className, onClick, onMouseOver, data-\*, etc.) |
Note: colIndex is automatically injected by Thead and should not be manually provided.
Body container component. Must wrap all Tr components.
Props:
| Prop | Type | Required | Default | Description |
| -------------- | --------------------- | -------- | ------- | --------------------------------------------------------------------------------- |
| offsetHeight | number | No | 45 | Height offset for calculating total height |style
| | React.CSSProperties | No | - | Custom styles for the body container |...props
| | HTMLDivElement | No | - | All standard HTML div attributes (className, onClick, onMouseOver, data-\*, etc.) |
Note:
- Tbody automatically injects rowIndex prop to all Tr children. The rowIndex will be the absolute index in the data array (accounting for virtualization).Tbody
- automatically calculates totalHeight and totalWidth from table context.Tbody
- only renders visible rows (plus overscan rows) to optimize performance.offsetHeight
- The prop is optional and used to calculate the total height (defaults to 45px). Typically you can omit this prop and use defaults.
- All standard HTML div attributes are accepted and will be applied to the body container element.
Row component. Must be used inside Tbody.
Props:
| Prop | Type | Required | Default | Description |
| ---------- | --------------------- | -------- | ------- | --------------------------------------------------------------------------------- |
| rowIndex | number | No | - | Automatically injected by Tbody |style
| | React.CSSProperties | No | - | Custom styles for the row |...props
| | HTMLDivElement | No | - | All standard HTML div attributes (className, onClick, onMouseOver, data-\*, etc.) |
Note:
- rowIndex is automatically injected by Tbody and should not be manually provided.Tr
- automatically injects colIndex prop to all Td children.
- All standard HTML div attributes are accepted and will be applied to the row element.
Cell component. Must be used inside Tr.
Props:
| Prop | Type | Required | Default | Description |
| ---------- | --------------------- | -------- | ------- | --------------------------------------------------------------------------------- |
| colIndex | number | No | - | Automatically injected by Tr |style
| | React.CSSProperties | No | - | Custom styles for the cell |...props
| | HTMLDivElement | No | - | All standard HTML div attributes (className, onClick, onMouseOver, data-\*, etc.) |
Note:
- colIndex is automatically injected by Tr and should not be manually provided.Th
- The cell width automatically matches the corresponding width from the header.
- All standard HTML div attributes are accepted and will be applied to the cell element.
Performance Note: Even with 10,000 rows, only ~15-20 DOM nodes (visible rows + overscan) are rendered at any time. The table maintains smooth scrolling and low memory usage regardless of dataset size.
The table automatically handles row virtualization. Only visible rows (plus overscan rows) are rendered in the DOM, making it performant even with thousands of rows.
Important:
- Pass totalData (the total number of rows) to the Table componentTbody
- When mapping over data in , map over the entire dataset. The table handles virtualization internally by:rowIndex
1. Calculating which rows are visible based on scroll position
2. Rendering spacers above and below visible rows to maintain correct scroll height
3. Automatically injecting correct values to Tr components
4. Only rendering visible rows in the DOM
All components extend standard HTML div attributes and can be styled via:
- Inline style prop (available on all components)containerStyle
- prop on Table component for container-specific stylingclassName
- Standard HTML attributes (, onClick, onMouseOver, data-* attributes, etc.)
- CSS targeting the component elements via className
Note: Since all components render as div elements, you can apply any standard HTML div attributes to them.
Full TypeScript support is included. The Table component is generic and accepts a type parameter:
` - Violating these requirements will throw helpful error messages. - Column widths are defined declaratively via tsx totalData={myData.length} ...>
`ThComponent Hierarchy Requirements
must be used inside TheadTr
- must be used inside TbodyTd
- must be used inside TrTable
- must wrap everythingThNotes
componentsrowHeight
- Row heights are consistent (controlled by prop)height
- The table supports horizontal scrolling when content is wider than container
- Header is sticky and remains visible while scrolling
- All components accept standard HTML div attributes for maximum flexibility
- The prop on Table` is optional, but recommended for proper virtualization behavior