npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@loykin/gridkit

v0.3.1

Published

A feature-rich React DataGrid component with virtualization, sorting, filtering, and pagination.

Readme

@loykin/gridkit

GridKit is a React data view library built on TanStack Table.

npm package

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, number types
  • Global Search — debounced toolbar search across configurable columns
  • Pagination — flexible placement: footer, toolbar, or fully external via onTableReady
  • Infinite ScrollDataGridInfinity with IntersectionObserver-based next-page loading
  • Row Drag ReorderDataGridDrag for sortable rows via dnd-kit
  • Column Resizing — drag-to-resize with onChange or onEnd policy
  • 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
  • classNames Slots — 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 icons prop
  • Escape HatchtableOptions passes advanced TanStack Table options safely

Installation

npm install @loykin/gridkit

Peer Dependencies

npm install react react-dom @tanstack/react-table @tanstack/react-virtual

Quick 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.

DataGridClassNamesDataGrid, 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)
}

DataGridCardClassNamesDataGridCard

interface DataGridCardClassNames extends GridKitClassNames {
  card?: string  // individual card item (.gridkit-card)
}

DataGridListClassNamesDataGridList

interface DataGridListClassNames extends GridKitClassNames {
  item?: string  // individual list item (.gridkit-list-item)
}

DataGridChatClassNamesDataGridChat

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
}

DataGridAgentChatClassNamesDataGridAgentChat

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

  • DataGridCard virtualization 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 call onCommit or onCancel. 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:e2e

The 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: auto to 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. For DataGridCard and DataGridList, set enableVirtualization to 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 fillParent as an alias for tableHeight="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:

  1. Add the format spec to your system prompt so the AI outputs gridkit-table JSON.
  2. Parse the response and pass it to <GridKitAutoTable payload={...} />.
  3. 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 fences

Usage

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,
) => TQuery

inferTablePayload

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 DragHandleCell in the cell of 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