@loykin/gridkit
v0.3.1
Published
A feature-rich React DataGrid component with virtualization, sorting, filtering, and pagination.
Maintainers
Readme
@loykin/gridkit
GridKit is a React data view library built on TanStack Table.
TanStack Table gives you the engine. GridKit gives you the rendered UI: tables, lists, cards, chat timelines, and agent event streams — all sharing the same sorting, filtering, search, pagination, virtualization, backend query state, and theming pipeline.
It is not headless. It owns the structure for common product data views so you
do not have to rebuild headers, filter controls, pagination, scroll layout,
loading states, empty states, and design-system styling for every screen. The
tradeoff is that you style the rendered structure rather than build it from
scratch. CSS variables handle most theming; classNames slots cover the rest.
When to use
- You want TanStack Table's state model without assembling the rendered UI from primitives.
- You have an existing design system and need the grid to adopt its colors and spacing — not fight them.
- You need more than a table — list, card grid, or chat timeline with the same filtering and data pipeline.
- You are building admin tools, dashboards, resource browsers, datasource lists, product catalogs, or other data-heavy product screens.
- You are building an LLM or AI agent UI — GridKit ships a chat timeline view (
DataGridAgentChat) and a structured-output table renderer (GridKitAutoTable) that turns AI-returned JSON directly into a sortable, filterable grid.
If you want full rendering control with zero default markup, use TanStack Table directly. GridKit is the layer above it.
If you need spreadsheet-style editing, copy/paste, and keyboard-heavy data
entry, a spreadsheet-focused grid such as react-data-grid may be a better fit.
View variants
| Component | Output | Shared features |
|---|---|---|
| DataGrid | <table> with header, body, footer | All |
| DataGridInfinity | Same as DataGrid with infinite scroll | All |
| DataGridDrag | Same as DataGrid with row drag-reorder | All |
| DataGridCard | Responsive card grid | Filtering, sorting, infinite scroll |
| DataGridList | Custom item renderer in a list | Filtering, sorting, search, infinite scroll |
| DataGridChat | Message timeline (top-load, stick-to-bottom) | Filtering, sorting, search |
| DataGridAgentChat | Agent event stream (messages, tool calls, artifacts, status) | Filtering, sorting, search |
| GridKitAutoTable | Renders GridKitTablePayload JSON from an LLM directly as a DataGrid | Sorting, all DataGrid props |
| GridKitTable | Query-driven table: takes a def (JSON block) + executor (backend connector), fetches and renders automatically | Sorting, all DataGrid props |
All variants share the same column definition, DataStore, and filter/sort/search pipeline.
Features
- Virtualization — only visible rows are rendered via
@tanstack/react-virtual - Sorting — client-side and server-side (manual)
- Column Filters — filter row or icon-mode with
text,select,multi-select,numbertypes - Global Search — debounced toolbar search across configurable columns
- Pagination — flexible placement:
footer, toolbar, or fully external viaonTableReady - Infinite Scroll —
DataGridInfinitywith IntersectionObserver-based next-page loading - Row Drag Reorder —
DataGridDragfor sortable rows via dnd-kit - Column Resizing — drag-to-resize with
onChangeoronEndpolicy - Column Pinning — pin columns left or right
- Column Visibility — show/hide columns via toolbar dropdown
- Row Selection — checkbox selection with select-all support
- Row Actions — per-row action menu defined at column level
- Row Expansion — tree rows with collapsible sub-rows
- DataStore — map-based external store for high-frequency real-time updates
- Server-Side Support — sorting, filtering, and pagination all controllable externally
- CSS Theming — override
--gridkit-*variables to match any design system classNamesSlots — apply custom classes to any structural element (container, header, footer, row, cell, empty state, load-more)- Icon Overrides — replace any built-in icon via the
iconsprop - Escape Hatch —
tableOptionspasses advanced TanStack Table options safely
Installation
npm install @loykin/gridkitPeer Dependencies
npm install react react-dom @tanstack/react-table @tanstack/react-virtualQuick Start
import { DataGrid, type DataGridColumnDef } from '@loykin/gridkit'
import '@loykin/gridkit/styles'
type User = {
id: number
name: string
email: string
}
const columns: DataGridColumnDef<User>[] = [
{ accessorKey: 'id', header: 'ID' },
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
]
const rows: User[] = [
{ id: 1, name: 'Ada Lovelace', email: '[email protected]' },
{ id: 2, name: 'Grace Hopper', email: '[email protected]' },
]
export function UsersGrid() {
return <DataGrid data={rows} columns={columns} tableHeight={400} />
}CSS Setup
Import the stylesheet once in your app entry point:
import '@loykin/gridkit/styles'Theming
GridKit has two customization surfaces:
1. CSS variables — colors, spacing, fonts, backgrounds. If it's a design token, it goes here.
:root {
--gridkit-header-background: #0f172a;
--gridkit-header-foreground: #f8fafc;
--gridkit-border: #e2e8f0;
--gridkit-radius: 0.75rem;
}2. classNames prop — structural class injection. Use this for layout utilities, shadows, and hover effects that CSS variables cannot express — not for colors or spacing that already have a --gridkit-* token.
// Good — structure/layout that has no --gridkit-* equivalent
<DataGrid
classNames={{
frame: 'shadow-md',
row: 'hover:bg-blue-50',
footer: 'border-t',
}}
...
/>
// Avoid — use CSS variables instead
<DataGrid
classNames={{
header: 'bg-slate-900 text-white', // → --gridkit-header-background / --gridkit-header-foreground
cell: 'px-4', // → --gridkit-cell-padding (if exposed)
}}
...
/>With shadcn/ui — works out of the box. The --gridkit-* variables automatically fall back to your existing shadcn CSS variables.
Standalone — hardcoded defaults are applied automatically. No configuration needed.
Custom theme — override only what you need:
:root {
--gridkit-background: #ffffff;
--gridkit-foreground: #0a0a0a;
--gridkit-border: #e5e7eb;
--gridkit-primary: #3b82f6;
--gridkit-muted: #f5f5f5;
--gridkit-muted-foreground: #6b7280;
--gridkit-header-background: var(--gridkit-muted);
--gridkit-header-foreground: var(--gridkit-muted-foreground);
--gridkit-header-border: var(--gridkit-border);
--gridkit-header-control-background: var(--gridkit-header-background);
--gridkit-header-control-foreground: var(--gridkit-header-foreground);
--gridkit-header-control-border: var(--gridkit-header-border);
--gridkit-header-popover-background: var(--gridkit-header-background);
--gridkit-header-popover-foreground: var(--gridkit-header-foreground);
--gridkit-header-popover-border: var(--gridkit-header-border);
--gridkit-control-background: var(--gridkit-background);
--gridkit-control-foreground: var(--gridkit-foreground);
--gridkit-control-border: var(--gridkit-border);
--gridkit-footer-background: var(--gridkit-background);
--gridkit-footer-foreground: var(--gridkit-muted-foreground);
--gridkit-footer-border: var(--gridkit-border);
--gridkit-radius: 0.5rem;
}
.dark {
--gridkit-background: #0a0a0a;
--gridkit-foreground: #fafafa;
--gridkit-border: rgba(255, 255, 255, 0.1);
--gridkit-primary: #6366f1;
--gridkit-muted: #1a1a1a;
--gridkit-muted-foreground: #a1a1aa;
--gridkit-header-background: var(--gridkit-muted);
--gridkit-header-foreground: var(--gridkit-muted-foreground);
--gridkit-header-border: var(--gridkit-border);
--gridkit-header-control-background: var(--gridkit-header-background);
--gridkit-header-control-foreground: var(--gridkit-header-foreground);
--gridkit-header-control-border: var(--gridkit-header-border);
--gridkit-header-popover-background: var(--gridkit-header-background);
--gridkit-header-popover-foreground: var(--gridkit-header-foreground);
--gridkit-header-popover-border: var(--gridkit-header-border);
--gridkit-control-background: var(--gridkit-background);
--gridkit-control-foreground: var(--gridkit-foreground);
--gridkit-control-border: var(--gridkit-border);
--gridkit-footer-background: var(--gridkit-background);
--gridkit-footer-foreground: var(--gridkit-muted-foreground);
--gridkit-footer-border: var(--gridkit-border);
}UI adapters
GridKit owns table structure, virtualization, and state. An optional UI adapter supplies two recipes:
- component recipe: Badge, Button, Input, Checkbox, Select, and Popover
- appearance recipe: a scoped root class, design tokens, and synchronized metrics for the grid frame, header, rows, cells, selection column, controls, footer, borders, typography, hover, focus, and elevation
This keeps one stable grid DOM while making the entire grid belong to the
selected design system. Without uiAdapter, the built-in components and
appearance continue to work exactly as before. Missing component slots fall
back to the built-in components.
Material UI
Install Material UI in the consuming application, then use the optional subpath:
import { DataGrid } from '@loykin/gridkit'
import { MuiGridKitProvider } from '@loykin/gridkit/adapters/mui'
import { ThemeProvider, createTheme } from '@mui/material/styles'
const theme = createTheme()
<ThemeProvider theme={theme}>
<MuiGridKitProvider preset="data-grid" density="standard">
<DataGrid data={rows} columns={columns} />
</MuiGridKitProvider>
</ThemeProvider>muiAdapter is also exported as a ready-to-use adapter created from MUI's
default theme. Use createMuiAdapter(theme) when the application has a custom
MUI theme and a provider is not convenient. MuiGridKitProvider reads the
active MUI Theme automatically, so palette, shape, typography, elevation, and
real MUI controls stay in sync when the theme changes.
MUI has two distinct table families, so the adapter keeps their native sizing vocabulary:
// MUI X DataGrid-compatible visual configuration
<MuiGridKitProvider
preset="data-grid"
density="compact"
columnHeaderHeight={48}
rowHeight={40}
>
<DataGrid data={rows} columns={columns} />
</MuiGridKitProvider>
// Material UI Table-compatible visual configuration
<MuiGridKitProvider preset="table" size="small">
<DataGrid data={rows} columns={columns} />
</MuiGridKitProvider>The data-grid preset defaults to MUI X standard geometry (56px header, 52px
rows). It accepts MUI X's density, columnHeaderHeight, and rowHeight
names. The table preset accepts Material UI Table's size="small | medium".
GridKit data, column, and feature props remain unchanged because GridKit still
owns the table engine.
Theme overrides for the real adapter components (MuiButton, MuiCheckbox,
MuiTextField, MuiSelect, MuiPopover, and MuiChip) apply normally.
MuiDataGrid and MuiTableCell component overrides do not apply because
GridKit intentionally retains its own virtualized grid DOM; customize those
structural slots through GridKit tokens, classNames, or styles.
shadcn/ui
shadcn components live inside the consuming application, so pass those local components to the factory:
import { DataGrid } from '@loykin/gridkit'
import { createShadcnAdapter } from '@loykin/gridkit/adapters/shadcn'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
const shadcnAdapter = createShadcnAdapter({
Badge,
Button,
Input,
Checkbox,
Popover,
PopoverTrigger,
PopoverContent,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
})
<DataGrid uiAdapter={shadcnAdapter} data={rows} columns={columns} />The shadcn adapter uses the standard shadcn table recipe by default and accepts
the same density options as its optional second argument.
Adapter metrics are defaults, not locks. Explicit grid configuration wins:
<DataGrid
uiAdapter={muiAdapter}
headerHeight={44}
rowHeight={36}
checkboxConfig={{ ...checkboxConfig, columnWidth: 52 }}
/>headerHeight is a layout input, not just a paint override. GridKit uses the
resolved value for grouped-header geometry and also exposes it as
--gridkit-header-height so header controls remain aligned. An explicit
headerHeight prop overrides the adapter metric; changing only the CSS variable
does not change the layout plan.
rowHeight follows the same resolution path: the resolved value drives the row
DOM geometry, virtualizer estimate, and --gridkit-row-height. Use
estimateRowHeight only when variable-height rows need a different initial
virtualizer estimate.
Use GridKitProvider when the same adapter should apply to multiple views:
<GridKitProvider adapter={muiAdapter}>
<DataGrid data={rows} columns={columns} />
</GridKitProvider>Adapter boundaries are intentionally one-way:
- GridKit core owns data, state, column capabilities, feature flags, event semantics, virtualization, and layout structure.
- Adapters may replace declared UI component slots and provide scoped visual tokens or geometry metrics.
- Adapters must not enable or disable sorting, filtering, grouping, selection, pinning, hiding, reordering, editing, or other core behavior.
- Without an adapter, component markup, feature availability, events, and default geometry remain backward-compatible.
All --gridkit-* Variables
| Variable | Description |
|---|---|
| --gridkit-background | Table / cell background |
| --gridkit-foreground | Default text color |
| --gridkit-popover | Dropdown / popover background |
| --gridkit-popover-foreground | Dropdown text color |
| --gridkit-primary | Primary accent (active page button, checkboxes) |
| --gridkit-primary-foreground | Text on primary backgrounds |
| --gridkit-secondary | Secondary background |
| --gridkit-secondary-foreground | Secondary text |
| --gridkit-muted | Muted surface background |
| --gridkit-muted-foreground | Muted text (placeholders, hints) |
| --gridkit-header-background | Table header and filter row background. Defaults to --gridkit-muted |
| --gridkit-header-foreground | Table header text color. Defaults to --gridkit-muted-foreground |
| --gridkit-header-border | Header row, header cell, and filter row border color. Defaults to --gridkit-border |
| --gridkit-header-control-background | Header filter input/select background. Defaults to --gridkit-header-background |
| --gridkit-header-control-foreground | Header filter input/select text color. Defaults to --gridkit-header-foreground |
| --gridkit-header-control-border | Header filter input/select/checkbox border color. Defaults to --gridkit-header-border |
| --gridkit-header-control-placeholder | Header filter input placeholder color. Defaults to a translucent --gridkit-header-control-foreground |
| --gridkit-header-popover-background | Header-origin popover background. Defaults to --gridkit-header-background |
| --gridkit-header-popover-foreground | Header-origin popover text color. Defaults to --gridkit-header-foreground |
| --gridkit-header-popover-border | Header-origin popover border color. Defaults to --gridkit-header-border |
| --gridkit-accent | Hover / accent background |
| --gridkit-accent-foreground | Accent text |
| --gridkit-destructive | Destructive action color |
| --gridkit-border | Border color |
| --gridkit-input | Input border fallback. Defaults to framework --input when available |
| --gridkit-control-background | Input, select, and checkbox control background. Defaults to --gridkit-background |
| --gridkit-control-foreground | Input and select text color. Defaults to --gridkit-foreground |
| --gridkit-control-border | Input, select, and checkbox border color. Defaults to --gridkit-input |
| --gridkit-control-placeholder | Input placeholder color. Defaults to --gridkit-muted-foreground |
| --gridkit-popover-border | Generic popover and action menu border color. Defaults to a translucent --gridkit-foreground |
| --gridkit-popover-option-hover-background | Generic popover option hover background. Defaults to --gridkit-muted |
| --gridkit-popover-section-foreground | Generic popover section label color. Defaults to --gridkit-muted-foreground |
| --gridkit-footer-background | Footer surface background. Defaults to --gridkit-background |
| --gridkit-footer-foreground | Footer text color. Defaults to --gridkit-muted-foreground |
| --gridkit-footer-border | Footer border color token for custom footer styles. Defaults to --gridkit-border; the built-in footer wrapper does not draw a border by default |
| --gridkit-container-border | Outer frame border color. Defaults to --gridkit-border. Set to transparent to hide; use styles={{ frame: { border: 'none' } }} to remove the border entirely (no 1 px space) |
| --gridkit-ring | Focus ring color |
| --gridkit-radius | Border radius base value |
classNames and styles Reference
Each view accepts a classNames prop for class injection and a styles prop for inline CSSProperties. Both share the same slot keys.
Base slots — all views
interface GridKitClassNames {
root?: string // outermost shell (.gridkit-shell)
toolbar?: string // toolbar wrapper (.gridkit-toolbar-frame)
frame?: string // bordered outer frame — carries border/radius (.gridkit-frame)
frameInner?: string // inner scroll/clip container (.gridkit-frame-inner)
content?: string // inner data region (view-specific element)
header?: string // header area (.gridkit-header) — Table only
body?: string // body wrapper (.gridkit-body-wrapper) — Table only
footer?: string // footer area (.gridkit-footer)
empty?: string // empty state (.gridkit-empty)
error?: string // error state (.gridkit-error)
loading?: string // loading skeleton (.gridkit-loading-cell)
loadMore?: string // infinite scroll trigger — Table, Card, List
}header and body are Table-only slots. On Card/List/Chat they are accepted but ignored.
DataGridClassNames — DataGrid, DataGridInfinity, DataGridDrag
interface DataGridClassNames extends GridKitClassNames {
headerCell?: string // individual header cell (.gridkit-header-cell)
row?: string // body row (.gridkit-row)
cell?: string // body cell (.gridkit-cell)
}DataGridCardClassNames — DataGridCard
interface DataGridCardClassNames extends GridKitClassNames {
card?: string // individual card item (.gridkit-card)
}DataGridListClassNames — DataGridList
interface DataGridListClassNames extends GridKitClassNames {
item?: string // individual list item (.gridkit-list-item)
}DataGridChatClassNames — DataGridChat
interface DataGridChatClassNames extends GridKitClassNames {
messageWrapper?: string // per-message wrapper
daySeparator?: string // separator injected between days
unreadMarker?: string // unread marker injected before a message
typingIndicator?: string // typing indicator rendered after the latest message
loadPrevious?: string // load-previous sentinel wrapper
// note: loadMore is not used in Chat — chat loads backward via loadPrevious
}DataGridAgentChatClassNames — DataGridAgentChat
interface DataGridAgentChatClassNames extends DataGridChatClassNames {
event?: string // per-event shell
message?: string // message bubble
toolCall?: string // tool call event
toolResult?: string // tool result event
artifact?: string // artifact event
status?: string // status event
user?: string // user-role message
assistant?: string // assistant-role message
system?: string // system-role message
tool?: string // tool name label
label?: string // event type label
eventBody?: string // event body content
code?: string // code/JSON block
actions?: string // per-event action buttons
}styles merge policy
Internal styles (virtualization positions, column widths) are applied first; user styles override on top:
style={{ ...internalStyle, ...styles?.row }}The following properties are structural — overriding them will break layout:
| Slot | Protected properties |
|---|---|
| row | transform, position, height, width |
| cell | width, position, left, right |
| frame | overflow, height, maxHeight |
| body | overflow, height |
Use CSS variables (--gridkit-*) for colors, spacing, and typography. Reserve styles for CSS variable injection or cases where no token exists:
// Scope a CSS variable to one grid instance
<DataGrid
styles={{ frame: { '--gridkit-radius': '0px' } as React.CSSProperties }}
...
/>
// Remove the outer border entirely (including its 1 px space)
<DataGrid
styles={{ frame: { border: 'none' } }}
...
/>Basic Usage
import { DataGrid } from '@loykin/gridkit'
import '@loykin/gridkit/styles'
const columns = [
{ accessorKey: 'id', header: 'ID' },
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
]
export function MyTable() {
return (
<DataGrid
data={rows}
columns={columns}
tableHeight={400}
/>
)
}Performance
Keep data and columns references stable when the values are derived during render. TanStack Table recalculates row models when these references change, and sorting/filtering operate over the full row set even when the DOM is virtualized.
const columns = useMemo<DataGridColumnDef<User>[]>(
() => [
{ accessorKey: 'name' },
{ accessorKey: 'status', meta: { filterType: 'select' } },
],
[],
)
const data = useMemo(() => rowsFromQuery ?? [], [rowsFromQuery])For large table views, set a fixed tableHeight so virtualization can keep DOM work bounded to the visible rows plus overscan. DataGridList and DataGridCard support opt-in virtualization with enableVirtualization — a fixed containerHeight, tableHeight, fillContainer, or fillParent is required to bound the scroll container. DataGridChat is currently non-virtualized because prepend anchoring and bottom stickiness need stricter scroll handling.
Use fillContainer when a grid should fit inside an existing app panel without forcing short data to stretch. Use fillParent when the grid should always fill a parent-owned height. Both props work across all view variants (DataGrid, DataGridCard, DataGridList, DataGridChat).
Current Limits
DataGridCardvirtualization is opt-in (enableVirtualization). For large collections without a fixed height, add app-level paging or infinite loading instead.- Inline editing is basic cell editing: double-click enters
meta.editCell, and the editor must callonCommitoronCancel. Validation, row edit mode, async save states, and undo/redo are not built in. - Accessibility is partial. Table roles,
aria-sort, and popover semantics are present, but full keyboard grid navigation and screen-reader workflow testing are not complete. - Performance guidance is threshold-based rather than benchmark-based. Table virtualization turns on for fixed-height tables at 100+ rows; real app performance still depends on cell render cost, filter/sort cost, and data stability.
Test Coverage
Unit/integration tests cover sorting, header groups, date/datetime filters, chat scroll behavior, list virtualization, reverse infinite scroll, stick-to-bottom, and state persistence.
Browser E2E coverage is intentionally focused on regressions that jsdom cannot catch:
pnpm test:e2eThe E2E suite starts the playground and verifies column resize vs reorder separation, header group alignment, fill-container height behavior, datetime filter popover clipping, state persistence after reload, column visibility, runtime pinning, row actions, row selection, inline editing, tree expansion, and master-detail expansion.
Pagination
Pagination is opt-in. The pagination prop activates TanStack Table's pagination logic; the UI is injected separately so you can place it anywhere.
Pagination Components
| Component | Description | Best placement |
|---|---|---|
| DataGridPaginationBar | Full bar: rows-per-page dropdown + page info + nav buttons | footer |
| DataGridPaginationCompact | Minimal: < X / Y > nav only | headerRight (toolbar) |
| DataGridPaginationPages | Numbered pages: << < 1 2 [3] … 20 > >> | footer |
Placement Options
footer — below the grid
import { DataGrid, DataGridPaginationBar } from '@loykin/gridkit'
<DataGrid
data={rows}
columns={columns}
pagination={{ pageSize: 20 }}
footer={(table) => (
<DataGridPaginationBar table={table} className="grid-footer-pagination" pageSizes={[10, 20, 50]} />
)}
/>Footer pagination controls do not add spacing by default. Add the vertical gap at
the placement site with className so toolbar, footer, and external placements
can each own their layout.
.grid-footer-pagination {
padding-top: 8px;
}toolbar — inside the filter row
import { DataGrid, DataGridPaginationCompact } from '@loykin/gridkit'
<DataGrid
data={rows}
columns={columns}
pagination={{ pageSize: 20 }}
headerRight={(table) => <DataGridPaginationCompact table={table} />}
/>numbered pages
import { DataGrid, DataGridPaginationPages } from '@loykin/gridkit'
<DataGrid
data={rows}
columns={columns}
pagination={{ pageSize: 10 }}
footer={(table) => <DataGridPaginationPages table={table} className="grid-footer-pagination" siblingCount={2} />}
/>external — outside the DataGrid
import { DataGrid, DataGridPaginationBar } from '@loykin/gridkit'
const [table, setTable] = useState(null)
// Render anywhere — above, below, in a sidebar, etc.
{table && <DataGridPaginationBar table={table} />}
<DataGrid
data={rows}
columns={columns}
pagination={{ pageSize: 20 }}
onTableReady={(t) => setTable(t)}
/>Server-Side Pagination
Use initialPageIndex when GridKit owns the current page after mount. Use
pageIndex when your app owns the current page, such as URL-synced pagination
or resetting to page 0 after a parent resource changes.
const [pageIndex, setPageIndex] = useState(0)
<DataGrid
data={pageRows} // current page data only
columns={columns}
pagination={{
pageIndex,
pageSize: 20,
pageCount: Math.ceil(totalCount / 20), // tells TanStack total pages
onPageChange: (pageIndex, pageSize) => { // fetch on every page change
setPageIndex(pageIndex)
fetchPage(pageIndex, pageSize)
},
}}
footer={(table) => (
<DataGridPaginationBar table={table} className="grid-footer-pagination" totalCount={totalCount} />
)}
/>DataGridPaginationConfig
| Field | Type | Default | Description |
|---|---|---|---|
| pageSize | number | 20 | Initial page size |
| pageIndex | number | — | Controlled current page index (0-based) |
| initialPageIndex | number | 0 | Initial page index (0-based) |
| pageCount | number | — | Total page count for server-side (manual) pagination |
| onPageChange | (pageIndex, pageSize) => void | — | Called on every page or size change |
Fill Container Layout
Use fillContainer when the grid lives inside a fixed-height tab, drawer, split pane, or dashboard panel.
import { DataGrid, DataGridPaginationBar } from '@loykin/gridkit'
export function UsersPanel() {
return (
<div style={{ height: 520, minHeight: 0 }}>
<DataGrid
fillContainer
data={rows}
columns={columns}
pagination={{ pageSize: 50 }}
footer={(table) => <DataGridPaginationBar table={table} className="grid-footer-pagination" />}
/>
</div>
)
}Behavior:
- Short data uses natural table height, so the footer sits directly below the table.
- Overflowing data scrolls only inside the body area.
- The footer remains visible at the bottom of the parent panel.
- GridKit measures toolbar, header, footer, gaps, and parent resize internally; callers should not query internal GridKit class names to calculate
maxTableHeight.
Parent requirements:
- A fixed height,
height: 100%chain, or flex layout that gives the parent a real height. - In flex layouts, make sure the parent chain can shrink with
min-height: 0. - Do not add
overflow: autoto the GridKit frame or container elements; the scroll owner is the internal body element.
fillContainer works across all view variants. For DataGridCard and DataGridList, it also enables enableVirtualization without requiring an explicit containerHeight.
If you want a hard fixed table body regardless of content length, use tableHeight. If you want content to grow until a known cap, use maxTableHeight. If the cap depends on the surrounding app panel, use fillContainer.
Fill Parent Layout
Use fillParent when the parent layout already owns height and the grid should occupy that whole region.
import { DataGrid, DataGridPaginationBar } from '@loykin/gridkit'
export function MetricsTab() {
return (
<div className="h-full min-h-0 overflow-hidden">
<DataGrid
fillParent
data={rows}
columns={columns}
pagination={{ pageSize: 100 }}
footer={(table) => <DataGridPaginationBar table={table} className="grid-footer-pagination" />}
/>
</div>
)
}Behavior:
- The shell and scroll container participate in a full-height flex chain.
- Short data still fills the parent region, so the footer remains at the parent bottom.
- Overflowing data scrolls only inside the scroll area.
- For
DataGrid, large row sets use row virtualization automatically. ForDataGridCardandDataGridList, setenableVirtualizationto activate it.
fillContainer and fillParent solve different layout problems:
| Prop | Use when | Short data | Overflowing data |
|---|---|---|---|
| fillContainer | Parent height is a cap, but content should stay natural when short | Footer sits directly below the table | Body scrolls, footer remains visible |
| fillParent | Parent height is the layout contract and the grid should fill it | Footer stays at parent bottom | Body scrolls, footer stays at parent bottom |
Parent requirements:
- The direct parent must have a real height, or be inside a valid
height: 100%/ flex chain. - In flex layouts, the parent chain should allow shrinking with
min-height: 0. - Do not use
fillParentas an alias fortableHeight="100%"; use the prop so GridKit can set the internal flex and virtualization behavior correctly.
fillParent works across all view variants. For DataGridCard and DataGridList, it also enables enableVirtualization without requiring an explicit containerHeight.
If tableHeight and fillParent are both provided, tableHeight remains the explicit body height. Prefer using only one layout mode.
DataGridInfinity (Infinite Scroll)
import { DataGridInfinity } from '@loykin/gridkit'
export function MyInfiniteTable() {
const { data, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery(...)
return (
<DataGridInfinity
data={data}
columns={columns}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
fetchNextPage={fetchNextPage}
tableHeight={500}
/>
)
}DataGridList (Custom Row/List View)
Renders rows with your own item component instead of table markup. Columns still define the row schema for sorting, filtering, and global search; they do not have to be visible.
import { DataGridList, GlobalSearch, SelectFilter } from '@loykin/gridkit'
const columns = [
{ accessorKey: 'name' },
{ accessorKey: 'department', meta: { filterType: 'select' } },
{ accessorKey: 'status', meta: { filterType: 'select' } },
]
export function EmployeeList() {
return (
<DataGridList
data={employees}
columns={columns}
containerHeight={560}
itemGap={8}
itemPadding={12}
headerLeft={(table) => (
<SelectFilter table={table} columnId="department" label="Department" />
)}
headerRight={(table) => <GlobalSearch table={table} placeholder="Search…" />}
renderItem={(row) => (
<div className="rounded border p-3">
<strong>{row.original.name}</strong>
<span>{row.original.status}</span>
</div>
)}
/>
)
}With infinite scroll
<DataGridList
data={data}
columns={columns}
renderItem={(row) => <InboxRow item={row.original} />}
containerHeight={600}
enableVirtualization
estimateRowHeight={56}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
fetchNextPage={fetchNextPage}
/>DataGridList-only Props
List views use the shared row/data/filtering props, but omit table-only options such as column resizing, pinning, headers, and table width modes.
| Prop | Type | Default | Description |
|---|---|---|---|
| renderItem | (row: Row<T>) => ReactNode | — | Required. Render function for each list item |
| itemKey | (row: Row<T>) => string | row.id | Override the React key for each item |
| itemGap | number | 0 | Gap in px between list items |
| itemPadding | number | 0 | Padding in px around the list body |
| containerHeight | string \| number \| 'auto' | 'auto' | Preferred list container height |
| tableHeight | string \| number \| 'auto' | 'auto' | Compatibility alias for containerHeight |
| enableVirtualization | boolean | false | Render only the visible item window. Requires a fixed containerHeight, tableHeight, fillContainer, or fillParent |
| estimateRowHeight | number | 48 | Estimated item height in px for virtualization |
| overscan | number | 10 | Items rendered outside the visible window when virtualized |
| headerLeft | ReactNode \| (table: Table<T>) => ReactNode | — | Toolbar content on the left. Function form receives the table instance |
| headerRight | ReactNode \| (table: Table<T>) => ReactNode | — | Toolbar content on the right. Function form receives the table instance |
| footer | ReactNode | — | Static content below the list |
| hasNextPage | boolean | — | Whether more pages exist |
| isFetchingNextPage | boolean | — | Show loading indicator at the bottom |
| fetchNextPage | () => void | — | Called when the sentinel enters the viewport |
| rootMargin | string | '100px' | IntersectionObserver rootMargin for early trigger |
| classNames | DataGridListClassNames | — | Slot-based class injection (root, frame, item, footer, …) |
| styles | DataGridListStyles | — | Slot-based inline styles — same keys as classNames |
List CSS variables
| Variable | Default | Description |
|---|---|---|
| --gridkit-list-gap | 0px | Gap between list items |
| --gridkit-list-padding | 0px | Padding around the list body |
DataGridChat (Message Timeline View)
Renders row data as a message timeline. It supports loading older rows from the top, preserving scroll offset after prepends, and automatically staying at the bottom when the user is already near the latest message.
import { DataGridChat } from '@loykin/gridkit'
const columns = [
{ accessorKey: 'author' },
{ accessorKey: 'body' },
{ accessorKey: 'createdAt' },
]
export function MessageTimeline() {
return (
<DataGridChat
data={messages}
columns={columns}
getRowId={(message) => message.id}
containerHeight={640}
hasPreviousPage={hasPreviousPage}
isFetchingPreviousPage={isFetchingPreviousPage}
fetchPreviousPage={fetchPreviousPage}
renderMessage={(row) => <MessageBubble message={row.original} />}
renderTypingIndicator={() => <TypingIndicator />}
/>
)
}DataGridChat-only Props
Chat views use the shared row/data/filtering props, but omit table-only options such as column resizing, pinning, headers, table width modes, and checkbox selection.
| Prop | Type | Default | Description |
|---|---|---|---|
| renderMessage | (row: Row<T>) => ReactNode | — | Required. Render function for each message |
| renderDaySeparator | (row, previousRow) => ReactNode | — | Optional non-row separator before a message |
| renderUnreadMarker | (row) => ReactNode | — | Optional non-row marker before a message |
| renderTypingIndicator | () => ReactNode | — | Optional content after the latest message |
| hasPreviousPage | boolean | — | Whether older rows exist |
| isFetchingPreviousPage | boolean | — | Show loading indicator at the top |
| fetchPreviousPage | () => void | — | Called when the top sentinel enters the viewport |
| rootMargin | string | '100px' | IntersectionObserver rootMargin for early trigger |
| stickToBottom | boolean | true | Auto-scroll when the user is already near the bottom |
| bottomThreshold | number | 48 | Distance in px considered “at bottom” |
| onAtBottomChange | (atBottom: boolean) => void | — | Called when bottom state changes |
| containerHeight | string \| number \| 'auto' | 'auto' | Preferred chat container height |
| tableHeight | string \| number \| 'auto' | 'auto' | Compatibility alias for containerHeight |
| footer | ReactNode | — | Static content below the chat container |
| classNames | DataGridChatClassNames | — | Slot-based class injection (root, frame, messageWrapper, loadPrevious, …) |
| styles | DataGridChatStyles | — | Slot-based inline styles — same keys as classNames |
DataGridAgentChat (Agent Event Stream)
Renders an LLM agent run as a scrollable event stream. Built on DataGridChat, it adds a typed event model covering messages, tool calls, tool results, artifacts, and status events — with per-event-type render slots and an adapter pattern for any provider format.
Basic usage
import { DataGridAgentChat } from '@loykin/gridkit'
import type { AgentChatEvent } from '@loykin/gridkit'
const events: AgentChatEvent[] = [
{ id: '1', type: 'message', role: 'user', content: 'Show me the deployment status.' },
{ id: '2', type: 'tool_call', name: 'get_deployments', input: { env: 'prod' }, status: 'running' },
{ id: '3', type: 'tool_result', name: 'get_deployments', output: { healthy: 12, degraded: 1 } },
{ id: '4', type: 'message', role: 'assistant', content: '1 deployment is degraded.' },
]
export function AgentRunViewer() {
return (
<DataGridAgentChat
events={events}
containerHeight={560}
/>
)
}Adapter pattern
Use input + adapter when your runtime emits provider-specific events instead of GridKit's normalized format:
import { DataGridAgentChat } from '@loykin/gridkit'
import type { AgentChatAdapter } from '@loykin/gridkit'
type VercelMessage = { id: string; role: string; content: string; toolInvocations?: unknown[] }
const vercelAdapter: AgentChatAdapter<VercelMessage[]> = (messages) =>
messages.map((m) => ({ id: m.id, type: 'message', role: m.role as AgentChatRole, content: m.content }))
export function ChatUI() {
const { messages } = useChat()
return (
<DataGridAgentChat
input={messages}
adapter={vercelAdapter}
containerHeight={560}
stickToBottom
/>
)
}Per-type render slots
Override rendering at any granularity without replacing the whole event renderer:
<DataGridAgentChat
events={events}
renderMessageContent={(event) => <Markdown>{String(event.content)}</Markdown>}
renderToolCall={(event) => <ToolCallCard name={event.name} input={event.input} status={event.status} />}
renderArtifact={(event) => event.kind === 'chart' ? <Chart data={event.data} /> : null}
/>DataGridAgentChat-only Props
Inherits all DataGridChat props except data, columns, renderMessage, and getRowId.
| Prop | Type | Description |
|---|---|---|
| events | readonly TEvent[] | Normalized agent events. Use when your runtime already emits GridKit events |
| input | TInput | Provider-specific input, converted via adapter |
| adapter | AgentChatAdapter<TInput, TEvent> | Converts provider events to GridKit's normalized event format. Accepts a function or { toEvents } object |
| columns | DataGridColumnDef<TEvent>[] | Override search/filter columns. Defaults to type, role, status, name, and content fields |
| getEventId | (event, index) => string | Override row identity. Defaults to event.id |
| renderEvent | (event, ctx) => ReactNode | Full event renderer override — takes precedence over all per-type slots |
| renderMessageContent | (event, ctx) => ReactNode | Override message body rendering only |
| renderToolCall | (event, ctx) => ReactNode | Override tool call rendering |
| renderToolResult | (event, ctx) => ReactNode | Override tool result rendering |
| renderArtifact | (event, ctx) => ReactNode | Override artifact rendering |
| renderStatus | (event, ctx) => ReactNode | Override status event rendering |
| renderEventActions | (event, ctx) => ReactNode | Inject action buttons into any event |
| classNames | DataGridAgentChatClassNames | Slot-based class injection (root, frame, event, message, toolCall, eventBody, code, actions, …) |
| styles | DataGridAgentChatStyles | Slot-based inline styles — same keys as classNames |
| getEventClassName | (event, ctx) => string | Per-event class hook |
| getEventStyle | (event, ctx) => CSSProperties | Per-event inline style hook |
Agent chat CSS variables
| Variable | Default | Description |
|---|---|---|
| --gridkit-agent-chat-font-size | 14px | Base font size for events |
| --gridkit-agent-chat-event-gap | 6px | Gap between event sections within a bubble |
| --gridkit-agent-chat-assistant-max-width | 760px | Max bubble width for assistant messages |
| --gridkit-agent-chat-user-max-width | 640px | Max bubble width for user messages |
| --gridkit-agent-chat-code-font-size | 12px | Font size for JSON/code preview blocks |
| --gridkit-agent-chat-user-background | — | User message bubble background |
| --gridkit-agent-chat-user-border | — | User message bubble border color |
GridKitAutoTable (AI-Generated Tables)
GridKitAutoTable renders a GridKitTablePayload — a lightweight JSON format designed for LLM output — directly as a sortable DataGrid. The intended flow is:
- Add the format spec to your system prompt so the AI outputs
gridkit-tableJSON. - Parse the response and pass it to
<GridKitAutoTable payload={...} />. - Sorting, filtering, and all other DataGrid props work as normal.
System Prompt
Add this to your LLM system prompt:
When returning tabular data, always use this exact JSON format:
{
"type": "gridkit-table",
"title": "<optional table title>",
"columns": [
{
"key": "<property name used in rows>",
"label": "<display header>",
"type": "<text | number | date | boolean>",
"align": "<left | center | right>"
}
],
"rows": [
{ "<key>": <value>, ... }
]
}
Rules:
- "type" must always be "gridkit-table"
- "key" values in columns must match the property names in rows
- Column "type" and "align" are optional; omit if not applicable
- Do not wrap the JSON in markdown code fencesUsage
import { GridKitAutoTable } from '@loykin/gridkit'
import type { GridKitTablePayload } from '@loykin/gridkit'
// Example AI response (parsed from LLM output)
const payload: GridKitTablePayload = {
type: 'gridkit-table',
title: 'Q1 Sales Report',
columns: [
{ key: 'rep', label: 'Sales Rep' },
{ key: 'region', label: 'Region' },
{ key: 'revenue', label: 'Revenue', type: 'number', align: 'right' },
{ key: 'closedAt', label: 'Last Close', type: 'date' },
{ key: 'active', label: 'Active', type: 'boolean', align: 'center' },
],
rows: [
{ rep: 'Alice Kim', region: 'APAC', revenue: 1820000, closedAt: '2024-03-28', active: true },
{ rep: 'Bob Choi', region: 'NA', revenue: 2540000, closedAt: '2024-03-30', active: true },
],
}
export function AiResponseTable() {
return (
<GridKitAutoTable
payload={payload}
enableSorting
tableHeight={400}
/>
)
}Column types
| type | Rendering |
|--------|-----------|
| text (default) | Raw string value |
| number | toLocaleString() formatted |
| date | toLocaleDateString() formatted |
| boolean | Yes / No |
GridKitTablePayload
interface GridKitTablePayload {
type: 'gridkit-table'
title?: string
columns: GridKitTableColumn[]
rows: Record<string, unknown>[]
}
interface GridKitTableColumn {
key: string
label: string
type?: 'text' | 'number' | 'date' | 'boolean'
align?: 'left' | 'center' | 'right'
}All standard DataGrid props (sorting, filtering, height, classNames, etc.) are supported — data and columns are the only omitted props since they are derived from the payload.
GridKitTablePayloadSchema
A ready-made JSON Schema object is exported alongside the TypeScript types. Use it directly as an AI tool/structured-output schema — no Zod or extra dependencies required.
import { GridKitTablePayloadSchema } from '@loykin/gridkit'
// Anthropic tool
const response = await client.messages.create({
tools: [{
name: 'return_table',
description: 'Return tabular data as a gridkit-table',
input_schema: GridKitTablePayloadSchema,
}],
})
// OpenAI structured output
const response = await client.chat.completions.create({
response_format: {
type: 'json_schema',
json_schema: { name: 'gridkit_table', schema: GridKitTablePayloadSchema },
},
})
// Zod (v4) — if you want runtime validation on the consumer side
import { z } from 'zod'
const schema = z.fromJSONSchema(GridKitTablePayloadSchema)
const result = schema.safeParse(rawResponse)Embedding in DataGridAgentChat
Pass GridKitAutoTable inside renderMessage to render AI-returned tables inline in a chat view:
import { DataGridAgentChat, GridKitAutoTable } from '@loykin/gridkit'
import type { GridKitTablePayload } from '@loykin/gridkit'
<DataGridAgentChat
adapter={adapter}
renderMessage={(row) => {
const msg = row.original
if (msg.type === 'tool_result' && isGridKitTable(msg.output)) {
return <GridKitAutoTable payload={msg.output as GridKitTablePayload} enableSorting />
}
return <div>{msg.content}</div>
}}
/>GridKitTable (Query-Driven Tables)
GridKitTable is a Grafana-panel-style component for page templates and AI/MCP-driven UIs. It takes a def (a JSON block describing what to fetch) and an executor (your backend or MCP connector), then owns the full lifecycle: fetch → infer columns → render.
The intended use case is page templates stored as JSON (e.g. in a database), where each block declares a query and a shared executor handles all data fetching. The AI or MCP agent generates def blocks; your app registers one executor per data source.
Basic usage
import { GridKitTable } from '@loykin/gridkit'
import type { GridKitTableDef, GridKitQueryExecutor } from '@loykin/gridkit'
// Define once at app level — like a Grafana data source
type MyQuery = { table: string; filter?: Record<string, unknown> }
const executor: GridKitQueryExecutor<MyQuery> = async (query) => {
const res = await fetch('/api/query', {
method: 'POST',
body: JSON.stringify(query),
})
return res.json() // raw rows[]
}
// def comes from a page template JSON or AI output
const def: GridKitTableDef<MyQuery> = {
type: 'gridkit-table',
title: 'Active Users',
query: { table: 'users', filter: { active: true } },
}
export function TemplatePage() {
return (
<GridKitTable
def={def}
executor={executor}
enableSorting
tableHeight={400}
/>
)
}With prepare — injecting runtime params into the query
Use prepare to transform the query before execution, e.g. to inject user-specific filters or pagination params from outside the stored template:
import type { GridKitQueryPrepare } from '@loykin/gridkit'
const prepare: GridKitQueryPrepare<MyQuery> = (query, params) => ({
...query,
filter: {
...query.filter,
// inject current user id at runtime, not baked into the template
tenant_id: currentUser.tenantId,
},
})
<GridKitTable def={def} executor={executor} prepare={prepare} />GridKitTable Props
All DataGrid props are supported except data and columns, which are derived from fetched rows via inferTablePayload. Additional props:
| Prop | Type | Description |
|---|---|---|
| def | GridKitTableDef<TQuery> | Required. JSON block definition — stored in page templates or generated by AI |
| executor | GridKitQueryExecutor<TQuery> | Required. (query) => Promise<rows[]> — your backend or MCP connector |
| prepare | GridKitQueryPrepare<TQuery> | Optional transform applied before execution — inject runtime params (auth, tenant, pagination) |
| inferOptions | Omit<InferTablePayloadOptions, 'title'> | Options forwarded to inferTablePayload (e.g. per-column hints) |
| renderLoading | () => ReactNode | Custom loading state |
| renderError | (error: unknown) => ReactNode | Custom error state |
Types
interface GridKitTableDef<TQuery = unknown> {
type: 'gridkit-table'
title?: string
query: TQuery
}
type GridKitQueryExecutor<TQuery = unknown> = (
query: TQuery,
) => Promise<Record<string, unknown>[]>
type GridKitQueryPrepare<TQuery> = (
query: TQuery,
params: QueryParams,
) => TQueryinferTablePayload
inferTablePayload converts raw DB rows into a GridKitTablePayload by inferring column types, labels, and widths automatically. It is also used internally by GridKitTable.
Use it directly when you already have rows from React Query, SWR, or any other source and want to skip writing column definitions:
import { GridKitAutoTable, inferTablePayload } from '@loykin/gridkit'
const { data: rows } = useQuery(['users'], fetchUsers)
const payload = inferTablePayload(rows ?? [], { title: 'Users' })
// → { type: 'gridkit-table', columns: [...auto-inferred], rows }
<GridKitAutoTable payload={payload} enableSorting tableHeight={400} />Inference rules
| Column attribute | Inference logic |
|---|---|
| type | boolean if all values are boolean; number if all numeric; date if all ISO 8601 strings; otherwise text |
| label | snake_case and camelCase converted to Title Case |
| align | 'right' for number columns; otherwise default |
| flex | Estimated proportional width — boolean → 0.6, ID/code keys → 0.7, number → 0.8, date → 1.0, description/content keys → 1.8, default → 1.0 |
InferTablePayloadOptions
interface InferTablePayloadOptions {
title?: string
hints?: Record<string, Partial<GridKitTableColumn>>
}Use hints to override any inferred field per column:
inferTablePayload(rows, {
title: 'Orders',
hints: {
total: { type: 'number', align: 'right', flex: 1.2 },
order_id: { label: 'Order #' },
},
})DataGridCard (Card / Gallery View)
Renders rows as a responsive card grid instead of a table. All filtering, sorting, global search, and infinite scroll work identically to DataGridInfinity — only the visual output changes.
Basic usage
import { DataGridCard, GlobalSearch } from '@loykin/gridkit'
const columns = [
{ accessorKey: 'name' },
{ accessorKey: 'category', meta: { filterType: 'select' } },
{ accessorKey: 'price' },
]
export function ProductGrid() {
return (
<DataGridCard
data={products}
columns={columns}
minCardWidth={240}
minColumns={2}
enableSorting
headerRight={(table) => <GlobalSearch table={table} placeholder="Search…" />}
renderCard={(row) => (
<div className="rounded-lg border p-4">
<h3 className="font-semibold">{row.original.name}</h3>
<p className="text-sm text-muted-foreground">{row.original.category}</p>
<p className="mt-2 font-medium">${row.original.price}</p>
</div>
)}
/>
)
}With infinite scroll
<DataGridCard
data={data}
columns={columns}
renderCard={(row) => <ProductCard product={row.original} />}
minCardWidth={240}
minColumns={2}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
fetchNextPage={fetchNextPage}
/>Layout modes
| Props | CSS generated | Behaviour |
|---|---|---|
| minCardWidth={240} | repeat(auto-fill, minmax(240px, 1fr)) | Responsive — 1 col on mobile, 4+ on desktop |
| minCardWidth={240} minColumns={2} | repeat(auto-fill, minmax(min(240px, 50%), 1fr)) | Responsive, but never fewer than 2 columns |
| cardColumns={4} | repeat(4, 1fr) | Always exactly 4 columns |
DataGridCard-only Props
All shared props apply. Additional props:
| Prop | Type | Default | Description |
|---|---|---|---|
| renderCard | (row: Row<T>) => ReactNode | — | Required. Render function for each card |
| minCardWidth | number | 240 | Minimum card width in px — column count adjusts automatically |
| minColumns | number | 1 | The grid never collapses below this number of columns |
| cardColumns | number | — | Fixed column count — overrides minCardWidth and minColumns |
| enableVirtualization | boolean | false | Render only the visible card row band. Requires a fixed containerHeight, tableHeight, fillContainer, or fillParent |
| estimateCardHeight | number | 200 | Estimated height in px of one card row band for virtualization |
| overscan | number | 3 | Card row bands to render outside the visible window when virtualized |
| hasNextPage | boolean | — | Whether more pages exist |
| isFetchingNextPage | boolean | — | Show loading indicator at the bottom |
| fetchNextPage | () => void | — | Called when the sentinel enters the viewport |
| rootMargin | string | '100px' | IntersectionObserver rootMargin for early trigger |
| footer | ReactNode | — | Static content below the card container |
| classNames | DataGridCardClassNames | — | Slot-based class injection (root, frame, card, footer, …) |
| styles | DataGridCardStyles | — | Slot-based inline styles — same keys as classNames |
Card CSS variables
| Variable | Default | Description |
|---|---|---|
| --gridkit-card-gap | 16px | Gap between cards |
| --gridkit-card-padding | 16px | Padding around the grid |
DataGridDrag (Row Reorder)
import { DataGridDrag, DragHandleCell } from '@loykin/gridkit'
const columns = [
{
id: 'drag',
size: 36,
enableResizing: false,
cell: () => <DragHandleCell />,
},
{ accessorKey: 'name', header: 'Name' },
]
export function MyDraggableTable() {
const [rows, setRows] = useState(data)
return (
<DataGridDrag
data={rows}
columns={columns}
getRowId={(row) => row.id}
onRowReorder={setRows}
/>
)
}Place
DragHandleCellin thecellof whichever column should act as the grab handle.
Column Definition
Columns follow @tanstack/react-table's ColumnDef with additional meta options:
import type { DataGridColumnDef } from '@loykin/gridkit'
const columns: DataGridColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
meta: {
flex: 1, // stretch proportionally to fill remaining width
minWidth: 100,
align: 'left', // 'left' | 'center' | 'right'
pin: 'left', // 'left' | 'right'
wrap: true, // allow multi-line cell content
filterType: 'text', // 'text' | 'select' | 'multi-select' | 'number' | 'date' | 'date-range' | 'datetime' | 'datetime-range' | 'custom' | false
filterParams: {
width: 260, // filter popover width for icon-mode filters
placeholder: 'Search name…',
},
},
},
{
id: 'actions',
header: '',
meta: {
actions: (row) => [
{ label: 'Edit', onClick: (row) => openEdit(row) },
{ label: 'Delete', onClick: (row) => deleteRow(row), variant: 'destructive' },
],
},
},
]Grouped headers use TanStack's nested columns shape:
const columns: DataGridColumnDef<User>[] = [
{
id: 'identity',
header: 'Identity',
columns: [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
],
},
{
id: 'activity',
header: 'Activity',
columns: [
{ accessorKey: 'status', header: 'Status' },
{ accessorKey: 'lastSeen', header: 'Last Seen' },
],
},
]Header Group Layout
headerGroupLayout controls how ungrouped leaf columns are rendered alongside group headers.
| Value | Behaviour |
|---|---|
| 'padded' (default) | Ungrouped leaf columns show a blank placeholder cell in the group row. Layout is uniform — all header rows are the same height |
| 'span' | Ungrouped leaf columns stretch to fill the full header height. No placeholder is rendered |
// padded (default) — blank cell above "ID", full height group headers
<DataGrid columns={columns} />
// span — "ID" occupies both rows, no blank placeholder
<DataGrid columns={columns} headerGroupLayout="span" />Group header resize is intentionally disabled. Group header width is always the sum of its leaf columns. Only leaf column resize handles are shown.
Column meta Reference
| Field | Type | Description |
|---|---|---|
| flex | number | Flex ratio — distributes remaining container width proportionally |
| width | number | Fixed preferred column width in px |
| autoSize | boolean | Auto-fit column width to content via canvas text measurement |
| minWidth | number | Minimum column width in px |
| maxWidth | number | Maximum column width in px |
| align | 'left' \| 'center' \| 'right' | Cell text alignment |
| pin | 'left' \| 'right' | Pin column at definition level |
| wrap | boolean | Allow multi-line content; row height adjusts automatically |
| cellOverflow | 'visible' \| 'hidden' | Override cell overflow per column. Default is clip. Use 'visible' for Badge, Avatar, or Chip components with border-radius or box-shadow. Content can escape the cell box but is still clipped by the table's scroll container — use a portal for dropdowns that must escape the table viewport |
| filterType | 'text' \| 'select' \| 'multi-select' \| 'number' \| 'date' \| 'date-range' \| 'datetime' \| 'datetime-range' \| 'custom' \| false | Filter input type for this column |
| filterParams.width | number | Filter popover width in px for icon-mode filter popovers and row-mode multi-select popups. Does not resize the column menu popover |
| filterParams.maxOptionsHeight | number | Multi-select option list max height in px. Defaults to 192 |
| filterParams.placeholder | string | Text filter placeholder. Defaults to Filter… |
| backend.field | string | Backend field name sent to DataStoreBackend params. Defaults to the column id |
| backend.filterType | 'text' \| 'multi-select' \| 'range' \| false | Override filterType for backend mode only |
| backend.sortable | boolean | Whether this column is sortable in backend mode |
| editCell | (props: EditCellProps<T, V>) => ReactNode | Inline cell editor triggered by double-click. Must call props.onCommit(value) or props.onCancel(). Requires onCellValueChange on the DataGrid |
| actions | (row: T) => Action[] | Row action menu items |
Props
Shared Props (DataGrid, DataGridInfinity, DataGridDrag, DataGridCard)
Data & Display
| Prop | Type | Default | Description |
|---|---|---|---|
| data | T[] | [] | Row data |
| dataStore | DataStore<T> | — | Map-based store for real-time updates. Mutually exclusive with data |
| queryMode | 'client' \| 'backend' | 'client' | In backend mode, sorting, filtering, search, and pagination call dataStore.query() |
| columns | DataGridColumnDef<T>[] | — | Column definitions |
| error | Error \| null | — | Display error state |
| isLoading | boolean | — | Show loading skeleton |
| emptyMessage | string | — | Message when data is empty |
| emptyContent | ReactNode | — | Custom empty state UI (overrides emptyMessage) |
| showHeader | boolean | true | Show/hide the header row |
| fillContainer | boolean | false | Fit inside an explicit parent height while keeping short content natural and scrolling only the body on overflow. Works across all view variants |
| fillParent | boolean | false | Fill a parent-owned height. The parent must supply a real height via a fixed size or a flex chain with min-height: 0. Works across all view variants |
| tableHeight | string \| number \| 'auto' | 'auto' | Fixed height — enables internal scroll and virtualization |
| maxTableHeight | string \| number | — | Cap height — grows with content up to this limit, then scrolls |
| minTableHeight | string \| number | — | Floor height — content shorter than this keeps minimum space |
| rowHeight | number | 33 | Row height in px (also sets virtualizer estimate) |
| estimateRowHeight | number | — | Override virtualizer estimate independently of rowHeight |
| overscan | number | 10 | Rows to render outside the visible area |
| bordered | boolean | false | Show vertical dividers between columns |
| tableWidthMode | 'spacer' \| 'fill-last' \| 'independent' | 'spacer' | How remaining horizontal space is distributed |
| onRowClick | (row: T) => void | — | Row click handler |
| rowCursor | boolean | false | Show pointer cursor on rows |
| classNames | DataGridClassNames | — | Slot-based class injection (root, frame, header, body, footer, row, cell, …) |
| styles | DataGridStyles | — | Slot-based inline styles — same keys as classNames. Use for CSS variable injection or structural overrides |
| icons | DataGridIcons | — | Override any built-in icon slot |
Headers
| Prop | Type | Default | Description |
|---|---|---|---|
| headerGroupLayout | 'padded' \| 'span' | 'padded' | Header group layout. span lets ungrouped leaf headers occupy the full grouped header height |
| enableColumnMenu | boolean | false | Show a ⋯ menu button inside each column header |
| renderColumnMenu | (col: Column<T>, table: Table<T>, close: () => void, ctx: ColumnMenuContext) => ReactNode | — | Custom column header menu. ctx provides pre-resolved canSort, canFilter, canPin flags |
Sorting
| Prop | Type | Default | Description |
|---|---|---|---|
| enableSorting | boolean | true | Enable column sorting |
| enableMultiSort | boolean | false | Enable Shift+click multi-column sorting |
| maxMultiSortColCount | number | 3 | Maximum sorted columns when multi-sort is enabled |
| initialSorting | SortingState | — | Initial sort state |
| onSortingChange | (s: SortingState) => void | — | Called on sort change |
| manualSorting | boolean | false | Disable client-side sort — handle externally |
Filtering
| Prop | Type | Default | Description |
|---|---|---|---|
| enableColumnFilters | boolean | false | Show per-column filter UI |
| filterDisplay | 'row' \| 'icon' | 'row' | Filter as dedicated row or icon inside header cell |
| customFilterComponents | Record<string, ComponentType<CustomFilterProps<T, any>>> | — | Register custom filter UI by filterType |
| manualFiltering | boolean | false | Disable client-side filtering — handle externally |
| columnFilters | ColumnFiltersState | — | Controlled column filter state |
| onColumnFiltersChange | (f: ColumnFiltersState) => void | — | Called on filter change |
| globalFilter | string | — | Controlled global search value |
| onGlobalFilterChange | (v: string) => void | — | Called on global search change |
| searchableColumns | string[] | — | Column keys included in global search |
| headerLeft | `ReactNode | (table: Table) => R
