@versini/ui-datagrid
v1.0.1
Published
A data grid component library for React built with div-based ARIA grid layout for maximum flexibility and accessibility. Features include:
Downloads
1,269
Readme
@versini/ui-datagrid
A data grid component library for React built with div-based ARIA grid layout for maximum flexibility and accessibility. Features include:
- Accessible: Uses ARIA grid roles for screen reader support
- Flexible Layout: Div-based structure allows for complex styling
- CSS Grid Columns: Define column widths with CSS Grid track sizes
- Infinite Scrolling: Progressive loading with IntersectionObserver
- Animated Height: Smooth height transitions when content changes
- Sorting: Built-in sorting utilities and sortable column headers
- Sticky Header/Footer: With optional blur effects
- Loading States: Built-in loading overlay with spinner
Installation
npm install @versini/ui-datagridEntry Points
Import only what you need for optimal bundle size:
// Core data grid
import { DataGrid } from "@versini/ui-datagrid/datagrid";
// Header, Body, Footer
import { DataGridHeader } from "@versini/ui-datagrid/header";
import { DataGridBody } from "@versini/ui-datagrid/body";
import { DataGridFooter } from "@versini/ui-datagrid/footer";
// Row and Cell
import { DataGridRow } from "@versini/ui-datagrid/row";
import { DataGridCell } from "@versini/ui-datagrid/cell";
import { DataGridCellSort } from "@versini/ui-datagrid/cell-sort";
// Infinite scroll (progressive loading)
import { DataGridInfiniteBody } from "@versini/ui-datagrid/infinite";
// Animated height wrapper
import {
AnimatedWrapper,
useAnimatedHeight
} from "@versini/ui-datagrid/animated";
// Sorting utilities
import {
sortByDate,
sortByNumber,
sortByString,
sortByBoolean,
sortItems,
getNextSortConfig,
getOppositeSortDirection,
toggleSortDirection,
SortDirections,
type SortConfig,
type SortComparator
} from "@versini/ui-datagrid/sorting";
// Constants
import {
SortDirections,
BlurEffects,
ThemeModes,
CellWrapper
} from "@versini/ui-datagrid/constants";Basic Usage
import { DataGrid } from "@versini/ui-datagrid/datagrid";
import { DataGridHeader } from "@versini/ui-datagrid/header";
import { DataGridBody } from "@versini/ui-datagrid/body";
import { DataGridRow } from "@versini/ui-datagrid/row";
import { DataGridCell } from "@versini/ui-datagrid/cell";
function MyTable({ data }) {
return (
<DataGrid maxHeight="400px" stickyHeader>
<DataGridHeader>
<DataGridRow>
<DataGridCell>Name</DataGridCell>
<DataGridCell>Email</DataGridCell>
</DataGridRow>
</DataGridHeader>
<DataGridBody>
{data.map((item) => (
<DataGridRow key={item.id}>
<DataGridCell>{item.name}</DataGridCell>
<DataGridCell>{item.email}</DataGridCell>
</DataGridRow>
))}
</DataGridBody>
</DataGrid>
);
}CSS Grid Columns
Use the columns prop to define column widths with CSS Grid track sizes:
<DataGrid columns={["200px", "1fr", "auto"]} maxHeight="400px" stickyHeader>
<DataGridHeader caption="User List">
<DataGridRow>
<DataGridCell>Name</DataGridCell>
<DataGridCell>Email</DataGridCell>
<DataGridCell>Actions</DataGridCell>
</DataGridRow>
</DataGridHeader>
<DataGridBody>
{data.map((item) => (
<DataGridRow key={item.id}>
<DataGridCell>{item.name}</DataGridCell>
<DataGridCell>{item.email}</DataGridCell>
<DataGridCell>Edit</DataGridCell>
</DataGridRow>
))}
</DataGridBody>
</DataGrid>Supported column values include:
- Fixed sizes:
"200px","10rem","5em" - Flexible sizes:
"1fr","2fr" - Auto sizing:
"auto","min-content","max-content" - Functions:
"minmax(100px, 1fr)","fit-content(200px)" - Repeat:
"repeat(3, 1fr)","repeat(auto-fill, minmax(100px, 1fr))" - CSS variables and calc:
"var(--col-width)","calc(100% - 200px)"
Infinite Scroll
For datasets with hundreds to thousands of rows, use DataGridInfiniteBody for progressive loading:
import { useState } from "react";
import { DataGrid } from "@versini/ui-datagrid/datagrid";
import { DataGridHeader } from "@versini/ui-datagrid/header";
import { DataGridRow } from "@versini/ui-datagrid/row";
import { DataGridCell } from "@versini/ui-datagrid/cell";
import { DataGridInfiniteBody } from "@versini/ui-datagrid/infinite";
import { AnimatedWrapper } from "@versini/ui-datagrid/animated";
function MyInfiniteTable({ data }) {
const [visibleCount, setVisibleCount] = useState(0);
return (
<AnimatedWrapper dependency={visibleCount}>
<DataGrid maxHeight="500px" stickyHeader>
<DataGridHeader caption={`Showing ${visibleCount} of ${data.length}`}>
<DataGridRow>
<DataGridCell>Name</DataGridCell>
<DataGridCell>Date</DataGridCell>
</DataGridRow>
</DataGridHeader>
<DataGridInfiniteBody
data={data}
batchSize={25}
threshold={5}
onVisibleCountChange={(count) => setVisibleCount(count)}
>
{(item) => (
<DataGridRow key={item.id}>
<DataGridCell>{item.name}</DataGridCell>
<DataGridCell>{item.date}</DataGridCell>
</DataGridRow>
)}
</DataGridInfiniteBody>
</DataGrid>
</AnimatedWrapper>
);
}The DataGridInfiniteBody component handles all the complexity internally:
- Progressive loading with IntersectionObserver
- Correct marker placement for seamless scrolling (marker is placed
thresholditems before the end) - Automatic data slicing and memoization
Jump to Row
Use the scrollToIndex imperative method to programmatically scroll to any row, even if it's not yet loaded:
import { useRef, useState } from "react";
import { DataGrid } from "@versini/ui-datagrid/datagrid";
import { DataGridHeader } from "@versini/ui-datagrid/header";
import { DataGridRow } from "@versini/ui-datagrid/row";
import { DataGridCell } from "@versini/ui-datagrid/cell";
import {
DataGridInfiniteBody,
type DataGridInfiniteBodyRef
} from "@versini/ui-datagrid/infinite";
function MyInfiniteTableWithJump({ data }) {
const [activeRowId, setActiveRowId] = useState(null);
const infiniteBodyRef = useRef<DataGridInfiniteBodyRef>(null);
const handleJumpToRow = (targetId) => {
// Find the index of the row with the target id
const targetIndex = data.findIndex((row) => row.id === targetId);
if (targetIndex !== -1) {
setActiveRowId(targetId);
infiniteBodyRef.current?.scrollToIndex(targetIndex);
}
};
return (
<div>
<button onClick={() => handleJumpToRow(1341)}>
Jump to row with id=1341
</button>
<DataGrid maxHeight="400px" stickyHeader>
<DataGridHeader>
<DataGridRow>
<DataGridCell>ID</DataGridCell>
<DataGridCell>Name</DataGridCell>
</DataGridRow>
</DataGridHeader>
<DataGridInfiniteBody
ref={infiniteBodyRef}
data={data}
batchSize={25}
>
{(item) => (
<DataGridRow
key={item.id}
active={activeRowId === item.id}
onClick={() => setActiveRowId(item.id)}
>
<DataGridCell>{item.id}</DataGridCell>
<DataGridCell>{item.name}</DataGridCell>
</DataGridRow>
)}
</DataGridInfiniteBody>
</DataGrid>
</div>
);
}The scrollToIndex method:
- If the row is already visible → smooth scrolls to it immediately
- If the row is not yet loaded → expands visible count first, then scrolls after render
Sorting
import { useState, useMemo } from "react";
import { DataGrid } from "@versini/ui-datagrid/datagrid";
import { DataGridHeader } from "@versini/ui-datagrid/header";
import { DataGridBody } from "@versini/ui-datagrid/body";
import { DataGridRow } from "@versini/ui-datagrid/row";
import { DataGridCell } from "@versini/ui-datagrid/cell";
import { DataGridCellSort } from "@versini/ui-datagrid/cell-sort";
import {
sortItems,
getNextSortConfig,
SortDirections
} from "@versini/ui-datagrid/sorting";
function MySortableTable({ data }) {
const [sortConfig, setSortConfig] = useState({
sortedCell: "name",
sortDirection: SortDirections.ASC
});
const sortedData = useMemo(() => {
return sortItems(
data,
sortConfig.sortedCell,
sortConfig.sortDirection,
"string"
);
}, [data, sortConfig]);
const handleSort = (cellId) => {
setSortConfig(getNextSortConfig(sortConfig, cellId));
};
return (
<DataGrid>
<DataGridHeader>
<DataGridRow>
<DataGridCellSort
cellId="name"
onSort={handleSort}
sortDirection={
sortConfig.sortedCell === "name"
? sortConfig.sortDirection
: false
}
sortedCell={sortConfig.sortedCell}
>
Name
</DataGridCellSort>
<DataGridCellSort
cellId="date"
onSort={handleSort}
sortDirection={
sortConfig.sortedCell === "date"
? sortConfig.sortDirection
: false
}
sortedCell={sortConfig.sortedCell}
>
Date
</DataGridCellSort>
</DataGridRow>
</DataGridHeader>
<DataGridBody>
{sortedData.map((item) => (
<DataGridRow key={item.id}>
<DataGridCell>{item.name}</DataGridCell>
<DataGridCell>{item.date}</DataGridCell>
</DataGridRow>
))}
</DataGridBody>
</DataGrid>
);
}Props
DataGrid
| Prop | Type | Default | Description |
| ------------------ | ----------------------------------------------- | ---------- | -------------------------------------- |
| mode | 'dark' \| 'light' \| 'system' \| 'alt-system' | 'system' | Theme mode |
| compact | boolean | false | Reduced padding |
| stickyHeader | boolean | false | Fixed header while scrolling |
| stickyFooter | boolean | false | Fixed footer while scrolling |
| blurEffect | 'none' \| 'small' \| 'medium' \| 'large' | 'none' | Blur intensity for sticky elements |
| maxHeight | string | - | Max height (required for sticky) |
| disabled | boolean | false | Show loading overlay with spinner |
| columns | string[] | - | CSS Grid track sizes for column widths |
| summary | string | - | Alternative text for screen readers |
| className | string | - | CSS class for the grid element |
| wrapperClassName | string | - | CSS class for the wrapper container |
DataGridHeader
| Prop | Type | Default | Description |
| ------------------ | ----------- | ------- | ---------------------------------------- |
| caption | ReactNode | - | Caption/title displayed above header row |
| captionClassName | string | - | CSS class for the caption element |
| className | string | - | CSS class for the header |
DataGridBody / DataGridFooter
| Prop | Type | Default | Description |
| ----------- | -------- | ------- | ------------------------- |
| className | string | - | CSS class for the element |
DataGridRow
| Prop | Type | Default | Description |
| ----------- | --------- | ------- | ------------------------------------------------------- |
| active | boolean | false | Show left border indicator on the first cell of the row |
| className | string | - | CSS class for the row |
DataGridCell
| Prop | Type | Default | Description |
| ------------- | ------------------------------- | -------- | --------------------------------- |
| align | 'left' \| 'center' \| 'right' | 'left' | Horizontal alignment |
| colSpan | number | 1 | Number of columns to span |
| borderLeft | boolean | false | Add vertical border on left side |
| borderRight | boolean | false | Add vertical border on right side |
| className | string | - | CSS class for the cell |
DataGridCellSort
| Prop | Type | Default | Description |
| ----------------- | ----------------------------------------------- | -------------- | ----------------------------------- |
| cellId | string | required | Unique identifier for the column |
| children | string | required | Label text for the column header |
| onSort | (cellId, direction?) => void | - | Handler when sort button is clicked |
| sortDirection | 'asc' \| 'desc' \| false | required | Current sort direction |
| sortedCell | string | required | ID of the currently sorted column |
| align | 'left' \| 'center' \| 'right' | 'left' | Horizontal alignment |
| mode | 'dark' \| 'light' \| 'system' \| 'alt-system' | 'alt-system' | Theme mode for sort button |
| focusMode | 'dark' \| 'light' \| 'system' \| 'alt-system' | 'alt-system' | Focus mode for sort button |
| slotLeft | ReactNode | - | Content to display left of label |
| slotRight | ReactNode | - | Content to display right of label |
| className | string | - | CSS class for the cell |
| buttonClassName | string | - | CSS class for the sort button |
DataGridInfiniteBody
| Prop | Type | Default | Description |
| ---------------------- | ------------------------------------------- | ------- | ------------------------------------------------- |
| data | T[] | required | The full dataset to render progressively |
| children | (item: T, index: number) => ReactNode | required | Render function for each row |
| batchSize | number | 20 | Items to show initially and add per scroll |
| threshold | number | 5 | Items before marker to allow seamless scrolling |
| rootMargin | string | '20px'| IntersectionObserver margin |
| onVisibleCountChange | (visibleCount: number, total: number) => void | - | Callback when visible count changes |
| className | string | - | CSS class for the body element |
| ref | React.Ref<DataGridInfiniteBodyRef> | - | Ref to access imperative methods |
DataGridInfiniteBodyRef (Imperative Handle)
| Method | Signature | Description |
| --------------- | ---------------------------- | -------------------------------------------------------- |
| scrollToIndex | (index: number) => void | Scroll to a row by index. Expands visible count if needed, then smooth scrolls. |
License
MIT
