@versini/ui-datagrid
v4.1.6
Published
A data grid component library for React built with div-based ARIA grid layout for maximum flexibility and accessibility. Features include:
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
- Row Virtualization: Windowing for large datasets — only viewport rows stay in the DOM
- 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";
// Virtualized body for large datasets
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 large datasets, use DataGridInfiniteBody. It virtualizes the rows (windowing via @tanstack/react-virtual): only the rows near the viewport stay mounted, so the DOM stays small and scrolling stays smooth no matter how many rows there are.
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}
estimatedRowHeight={48}
onVisibleCountChange={(count) => setVisibleCount(count)}
>
{(item) => (
<DataGridRow key={item.id}>
<DataGridCell>{item.name}</DataGridCell>
<DataGridCell>{item.date}</DataGridCell>
</DataGridRow>
)}
</DataGridInfiniteBody>
</DataGrid>
</AnimatedWrapper>
);
}DataGridInfiniteBody keeps columns (including intrinsic auto tracks) working because it renders rows in normal grid flow between two full-span spacer elements (not absolutely positioned). It windows against the DataGrid's own scroll container — created when you set maxHeight or a sticky header/footer — and otherwise against the page (window scroll). To scroll inside your own bounded container, set maxHeight on the DataGrid rather than relying on an outer wrapper.
Notes:
- Set
estimatedRowHeightclose to your average row height. It seeds the scrollbar before rows are measured; a good estimate minimizes scrollbar drift and makesscrollToIndexto far rows land accurately. Real heights are always measured from the DOM, so variable / wrapping rows work. It defaults to the grid's density (44, or 29 when theDataGridiscompact), so you usually only need to set it for unusually tall rows. - The grid exposes
aria-rowcountandaria-rowindex, and keeps keyboard focus on the grid if a focused row recycles out. - Known limitation: because off-screen rows are removed from the DOM, browser find-in-page (Ctrl+F) and uncontrolled per-row state (e.g. an open menu, an uncommitted input) don't persist when a row recycles. Lift such state to controlled props. This is inherent to DOM virtualization.
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}>
{(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:
- Centers the target row in the scroll viewport, mounting it first if it's off-screen
- Scrolls smoothly, unless the user prefers reduced motion (then it jumps instantly)
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
| Prop | Type | Default | Description |
| ------------ | --------- | ----------- | --------------------------------------- |
| noData | boolean | false | Display empty state instead of children |
| noDataText | string | 'No Data' | Custom text for the empty state |
| className | string | - | CSS class for the element |
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 |
| 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 virtualize |
| children | (item: T, index: number) => ReactNode | required | Render function for each row |
| overscan | number | 8 | Rows rendered beyond the viewport on each side |
| estimatedRowHeight | number | 44 / 29 | Initial row-height estimate (29 when compact) before measurement |
| onVisibleCountChange | (visibleCount: number, total: number) => void | - | Monotonic high-water mark — furthest row reached, plus one |
| noData | boolean | false | Display empty state instead of infinite scroll |
| noDataText | string | 'No Data' | Custom text for the empty state |
| 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 | Center a row by index, mounting it if off-screen. Smooth unless reduced motion is preferred. |
License
MIT
