@kxnyshk/data-grid
v0.3.0
Published
A premium, AG-Grid-inspired React data grid. TypeScript, Tailwind, virtualized, every feature toggleable.
Downloads
322
Maintainers
Readme
DataGrid
A premium React data grid with virtualization, grouping, inline editing, and full theming — drop in and ship.
DataGrid is a polished, AG-Grid-inspired React table built for admin panels, analytics dashboards, and internal tools. It renders smoothly at 50k+ rows, ships compiled CSS so consumers don't need their own Tailwind setup, and exposes every visible element as a semantic class hook for theming.
Highlights
- Virtualized rendering — only ~30 rows in the DOM regardless of dataset size, so 100k-row tables stay smooth
- Multi-column sort with priority badges — shift-click headers to layer sorts; the order shows in numbered chips
- Grouping with inline aggregations — collapse rows into buckets and see sum / avg / median per group at the header
- Inline cell editing — double-click, edit, commit on Enter; a green dot marks unsaved changes per cell
- Excel export with collapsible outlines — the
.xlsxcarries group titles plus Excel-native expand/collapse buttons - GitHub-style search syntax —
field:value, ranges (mrr:1000..5000), wildcards (%pend%), strict equals (field:=Value),/autocomplete, inline?help - Sparklines and heat maps — drop in trend visualizations and value-scaled cell backgrounds with one prop
- Saved views — name a snapshot of filters/sorts/columns/density, share as JSON, sync edits back to the view
- Semantic CSS hooks — every element has a
kx-dg-*class for overrides; no theme provider, no prop wiring
Quick start
Install
npm install @kxnyshk/data-gridPeer dependencies: react ^18 or ^19, react-dom ^18 or ^19. Everything else (@radix-ui/react-tooltip, clsx, tailwind-merge, react-icons, exceljs) is bundled — you install nothing else.
One-line stylesheet import
import "@kxnyshk/data-grid/style.css";Minimal example
import { DataGrid, type ColumnDef } from "@kxnyshk/data-grid";
import "@kxnyshk/data-grid/style.css";
interface User {
id: string;
name: string;
email: string;
status: "Active" | "Inactive" | "Pending";
mrr: number;
}
const users: User[] = [
{
id: "u_1",
name: "Mia Patel",
email: "[email protected]",
status: "Active",
mrr: 4200,
},
{
id: "u_2",
name: "Diego Lin",
email: "[email protected]",
status: "Pending",
mrr: 1800,
},
{
id: "u_3",
name: "Sara Ahmed",
email: "[email protected]",
status: "Active",
mrr: 9100,
},
];
const columns: ColumnDef<User>[] = [
{ field: "name", header: "Name", editable: true },
{ field: "email", header: "Email", width: 240 },
{ field: "status", header: "Status", type: "tag" },
{
field: "mrr",
header: "MRR",
type: "number",
aggregation: "sum",
aggFormat: (v) => `$${v.toLocaleString()}`,
},
];
export default function App() {
return (
<DataGrid<User> columns={columns} rows={users} rowKey="id" title="Users" />
);
}That's a fully working grid — sortable, filterable, searchable, editable on name, with a grand-total row summing mrr. No additional configuration required.
Core concepts
Columns
A ColumnDef<T> describes one column: its field (a key on your row), its header label, and optionally type (string / number / date / tag / custom) for type-aware filtering and rendering. Use render for custom cell output, valueGetter for computed values, and per-column flags (editable, sortable, pinnable, etc.) to opt into or out of grid-level behavior. The full interface is in the API reference.
Rows
Rows are plain objects shaped to your row type. Pass them through the rows prop. The grid identifies each row via rowKey (a field name) — this defaults to "id" if present on the row, falling back to the row index. A stable rowKey matters: selections, edits, and saved views are keyed by it.
State
Everything is client-side by default — filters, sort, grouping, selection, pagination, edits, view snapshots. The grid handles the data pipeline internally; you provide rows and columns. Saved views are the one piece of state that can be controlled externally (see Controlled mode). Server-side pagination, sort, and filter aren't supported yet.
Theming
The grid ships compiled CSS and a semantic class layer named kx-dg-*. Every visible element (toolbar buttons, header cells, rows, popovers, chips) carries one of these classes. Override any of them in your own stylesheet — no prop wiring, no theme provider, no CSS-in-JS. See the Theming section for the pattern.
Features
Sorting
Click a column header to sort ascending; click again for descending; click a third time to clear. Shift-click another header to add it as a secondary sort — the priority appears in a numbered badge on each sort icon. Per-column overrides disable sorting on specific columns.
<DataGrid
enableSorting // default true
enableMultiSort // default true
defaultSort={[{ field: "mrr", dir: "desc" }]}
columns={[
{ field: "name", header: "Name" },
{ field: "score", header: "Score", sortable: false }, // not sortable
]}
rows={rows}
/>Filtering
Each filterable column gets a popover with type-aware operators: contains, equals, beginsWith, endsWith, blank for strings; equals, gt, gte, lt, lte, between, blank for numbers. Filters apply live as you type. An amber chip in the toolbar shows the active filter count with a one-click clear.
<DataGrid
enableFiltering
defaultFilters={{
status: { operator: "equals", value: "Active" },
mrr: { operator: "gte", value: "1000" },
}}
columns={columns}
rows={rows}
/>Search syntax
The toolbar search bar accepts plain text (substring match across all cells) AND GitHub-style scoped tokens. Tokens render as styled chips inline as you type — a chip forms the moment you type the colon. The field's value extends across single spaces until the next recognized field token, so name:Mia Patel is one chip with the value "Mia Patel", while name:Mia mrr:>5000 splits cleanly into two chips. Press Enter to "exit" the current chip and start typing the next filter (it inserts a 2-space hard break and moves your cursor past it).
Click the ? icon at the left end of the search bar to see this reference inline.
acme plain substring across all cells
status:Active exact match on the status column (case-insensitive)
status:=Active STRICT case-sensitive equals
name:Mia Patel multi-word value, no quotes needed
name:%mia% wildcard contains
name:%patel ends with
name:mia% begins with
name:%mia patel% contains a multi-word phrase
mrr:>5000 numeric operator (also <, >=, <=)
mrr:-1000..5000 numeric range, inclusive both sides (signed)
name:'mia patel' quoted literal — preserves : and other special chars
'/api/users' quoted text — searches literal "/" (autocomplete suppressed)
/sta "/" triggers column-name autocomplete
↵ exit current chip, ready for next filter
⌘/ focus the search bar from anywhereField name matching. The token before : matches against either col.field or the column's on-screen header label (whitespace collapsed and lowercased). So firstname: works for a column whose header is "First Name" and field is firstName. Field name wins on conflict.
Smart-extend boundaries. Single spaces stay inside a chip's value. Two consecutive spaces (or pressing Enter) create a hard break, ending the chip. Quoted values (name:'Mia: VIP') preserve everything between the quotes literally — useful when your value contains colons, percent signs, or quote characters.
Invalid-token policy. Tokens that LOOK like filters but fail to validate (unknown field with a value, wrong-type operator, non-numeric in a numeric column, unclosed '/" quote, or wildcards combined with quotes like %"mia"%) force the grid to an empty result — so you get a consistent "no rows" state instead of silently seeing all data.
Mid-typing is forgiving. field: (empty value), mrr:> (operator without value), and unclosed % (legal endsWith form) are all treated as in-progress and don't trigger force-empty.
Inline cell editing
Double-click any cell on a column marked editable: true. Enter commits, Esc cancels, focus loss commits. Edited cells get a small green dot in the corner until you save them upstream via onCellEdit. Re-typing the same value clears the dot (whitespace-trimmed string compare).
<DataGrid
enableEditing
onCellEdit={(e) => {
console.log(`${e.field}: ${e.oldValue} → ${e.newValue}`);
// Persist e.newValue to your backend
}}
columns={[
{ field: "name", header: "Name", editable: true },
{ field: "email", header: "Email", editable: true },
]}
rows={rows}
/>Row selection
Checkboxes appear in the leftmost column when enableSelection is on. The header checkbox toggles all rows on the current page, with an indeterminate state when partially selected. A blue chip in the toolbar shows the selected count with a one-click clear.
<DataGrid
enableSelection
onSelectionChange={({ selectedRows, selectedIds }) => {
console.log(`Selected ${selectedRows.length} rows`);
}}
columns={columns}
rows={rows}
/>Column operations
Drag column headers to reorder, drag the right edge to resize (minimum 150px, enforced everywhere), or use the column kebab menu to pin to the left, hide, or group by that column. Every operation has a grid-level flag and a per-column override.
<DataGrid
enableColumnResize
enableColumnReorder
enableColumnPinning
enableColumnHiding
defaultPinnedColumns={["name"]}
defaultHiddenColumns={["internalNotes"]}
columns={columns}
rows={rows}
/>Pagination
Pagination is on by default with sensible page-size options. It hides automatically when grouping is active — paged group headers are rarely useful.
<DataGrid
enablePagination
defaultPageSize={50}
pageSizeOptions={[20, 50, 100, 500, 1000]}
columns={columns}
rows={rows}
/>Density
Three row heights: compact (34px), comfortable (42px), cozy (52px). Users toggle via the toolbar; you can set the default or fully control it.
<DataGrid defaultDensity="compact" columns={columns} rows={rows} />Tag columns
Set type: "tag" on a column and pass a tagColorMap to render the value as a colored chip. Common Tailwind color classes are pre-included in the compiled CSS, so chips render correctly out of the box without your own Tailwind setup.
const statusStyles = {
Active: {
bg: "bg-emerald-50",
text: "text-emerald-700",
border: "border-emerald-200",
dot: "bg-emerald-500",
},
Pending: {
bg: "bg-amber-50",
text: "text-amber-700",
border: "border-amber-200",
dot: "bg-amber-500",
},
Inactive: {
bg: "bg-gray-100",
text: "text-gray-600",
border: "border-gray-200",
dot: "bg-gray-400",
},
};
const columns: ColumnDef<User>[] = [
{ field: "status", header: "Status", type: "tag", tagColorMap: statusStyles },
];Sparklines
When a column's cell value is a numeric array, render an inline SVG mini-chart with the Sparkline helper. Each cell auto-scales to its own min/max, so a column of sparklines compares trend shape — not absolute magnitude.
import { Sparkline } from "@kxnyshk/data-grid";
const columns: ColumnDef<User>[] = [
{
field: "mrrTrend",
header: "MRR Trend",
render: (v) => <Sparkline values={v} />,
},
];Users can toggle all sparklines on or off via the toolbar Visuals menu.
Heat maps
Set heatMap on a numeric column and the cell background scales from white (low) to a tinted color (high) based on the column's filtered min/max. Built-in palettes: green, amber, red, blue, violet. Or pass a custom { from, to } gradient.
const columns: ColumnDef<User>[] = [
{ field: "mrr", header: "MRR", type: "number", heatMap: "green" },
{
field: "seats",
header: "Seats",
type: "number",
heatMap: { from: "#fff", to: "#fce7f3" },
},
];Saved views
A "saved view" is a snapshot of all UI state: filters, sort, hidden columns, pinned columns, column order, column widths, density, group-by, expanded groups, page size, search, selection, and visuals toggles. Users save them by name, switch between them, export them as .json, and re-sync the active view when it diverges.
Views can be uncontrolled (the grid manages them internally) or controlled via views + onViewsChange:
const [views, setViews] = useState<GridView[]>([]);
<DataGrid
views={views}
onViewsChange={setViews}
columns={columns}
rows={rows}
/>;Imported .json view files include a column signature, so the grid rejects views whose source schema doesn't match the current grid's columns.
Excel export
Click Export in the toolbar to download a real .xlsx file (not CSV — proper Office Open XML with merged cells, alignment, widths). The popover offers a scope tab (entire dataset / current page / selection) crossed with format (current view vs raw). When grouping is active, the exported file contains merged group title rows AND uses Excel's native outline grouping — so users can expand and collapse groups inside Excel itself.
exceljs is a direct dependency but lazy-loaded via dynamic import inside the export handler. It only gets fetched the first time a user clicks Export, so your initial bundle stays small (~150KB without exceljs vs. ~1MB+ with).
Row detail
Pass a rowDetail render prop and each row gets an expand chevron in the leftmost column. Clicking it reveals the rendered content beneath the row, anchored so it stays visible during horizontal scroll.
<DataGrid
rowDetail={(row) => (
<div className="space-y-2 text-sm">
<div>
<b>Created:</b> {row.createdAt}
</div>
<div>
<b>Last login:</b> {row.lastLogin}
</div>
<div>
<b>Notes:</b> {row.notes}
</div>
</div>
)}
columns={columns}
rows={rows}
/>Virtualization
It just works. The grid mounts only ~30 rows at a time regardless of dataset size, so 1k or 100k rows scroll at the same frame rate. No setup, no virtualization library to wire up, no row-height estimation.
Theming
The kx-dg-* class layer
Every element in the grid carries a semantic class name starting with kx-dg-. These classes have NO styles attached — they're pure override hooks. You write CSS targeting them; the cascade does the rest. No theme objects, no JSON tokens, no provider component.
The pattern: a base class on every element (e.g. kx-dg-row) plus state modifiers when the element has a non-CSS state (e.g. kx-dg-row-selected, kx-dg-row-editing). Browser-detectable states (:hover, :focus, :disabled) use standard pseudo-classes — no JS modifier needed.
Override example
Want selected rows in soft amber instead of the default blue?
/* your-app.css */
.kx-dg-row-selected {
background: #fef3c7;
}
.kx-dg-row-selected:hover {
background: #fde68a;
}That's the whole pattern.
Load order matters
Import your overrides after the library stylesheet:
import "@kxnyshk/data-grid/style.css";
import "./your-overrides.css"; // ← afterIf you're competing with a high-specificity Tailwind utility from the library and your rule loses the cascade, !important is a safe escape hatch:
.kx-dg-grand-total-row {
background: #1f2937 !important;
color: #f9fafb !important;
}Class reference
The most useful classes — there are many more state modifiers throughout the source.
| Class | What it targets |
| ----------------------------- | ----------------------------------------------------------- |
| kx-dg-grid | Root wrapper around the entire grid |
| kx-dg-toolbar | Top toolbar bar |
| kx-dg-toolbar-button | Any toolbar icon-button (Density, Columns, Export, Views…) |
| kx-dg-toolbar-button-active | Toolbar button in its open/active state |
| kx-dg-toolbar-chip | Rounded info chips (filter count, group, selection, sorts…) |
| kx-dg-header | The <thead> element |
| kx-dg-header-cell | A single column header <th> |
| kx-dg-header-cell-sorted | Header of a column currently sorted (also -asc / -desc) |
| kx-dg-header-cell-pinned | Header of a pinned column |
| kx-dg-header-cell-filtered | Header of a column with an active filter |
| kx-dg-row | A regular data row |
| kx-dg-row-selected | A row whose checkbox is checked |
| kx-dg-row-hovered | A row under the mouse (also matches .kx-dg-row:hover) |
| kx-dg-cell | Any body cell <td> |
| kx-dg-cell-pinned | Cell in a pinned column |
| kx-dg-cell-editing | Cell currently in edit mode |
| kx-dg-cell-edit-indicator | The green dot on a cell with an unsaved edit |
| kx-dg-group-header-row | Row that introduces a group bucket |
| kx-dg-grand-total-row | The sticky total row at the bottom |
| kx-dg-detail-row | Expanded detail row beneath a parent |
| kx-dg-filter-popover | Per-column filter popover |
| kx-dg-column-menu | Column kebab-menu (pin / group / hide) |
| kx-dg-pagination | Pagination bar at the bottom |
| kx-dg-empty-state | "No data" / "No results" overlay |
| kx-dg-tag | Tag chips rendered by type: "tag" columns |
About consumer-supplied class strings. The compiled CSS includes a safelist covering the full Tailwind color palette across bg-, text-, and border- for shades 50–900. So when you pass classes through tagColorMap like { bg: "bg-emerald-50", text: "text-emerald-700" }, they all render correctly. What ISN'T covered: arbitrary values (bg-[#abc]), opacity modifiers (bg-blue-500/30), or layout classes inside custom render functions. For those, you still want Tailwind in your own build.
API
DataGridProps
The grid is fully prop-driven. Every visible feature has a toggle, and most have sensible defaults.
Data (required)
| Prop | Type | Default | Description |
| --------- | ---------------- | --------------------------------- | --------------------- |
| columns | ColumnDef<T>[] | — | Column definitions |
| rows | T[] | — | Data rows |
| rowKey | keyof T | "id" if present, else row index | Unique row identifier |
Toolbar
| Prop | Type | Default | Description |
| ------------------- | --------- | ----------- | ------------------------------------------------ |
| showToolbar | boolean | true | Show the toolbar at all |
| title | string | — | Title text shown on the left |
| showTitle | boolean | true | Toggle the title |
| showRowCount | boolean | true | Show "N rows" / "filtered/total rows" chip |
| showSearch | boolean | true | Show the search input |
| searchPlaceholder | string | "Search…" | Placeholder text in the search input |
| showColumnsButton | boolean | true | Show the show/hide columns dropdown |
| showDensityToggle | boolean | true | Show the density selector |
| showExportButton | boolean | true | Show the Export button |
| showSavedViews | boolean | true | Show the Views button |
| showVisualsMenu | boolean | true | Show the heat-maps + sparklines toggles dropdown |
Selection
| Prop | Type | Default | Description |
| ------------------- | -------------------------------- | ------- | ---------------------------------------- |
| enableSelection | boolean | true | Show checkbox column + enable row select |
| onSelectionChange | (e: SelectionEvent<T>) => void | — | Fires when the selection set changes |
Sorting
| Prop | Type | Default | Description |
| ----------------- | ------------- | ------- | ------------------------------------------ |
| enableSorting | boolean | true | Allow clicking headers to sort |
| enableMultiSort | boolean | true | Shift-click headers to add secondary sorts |
| defaultSort | SortEntry[] | [] | Initial sort state |
Filtering
| Prop | Type | Default | Description |
| ----------------- | ----------------------------- | ------- | -------------------------------------- |
| enableFiltering | boolean | true | Show filter popovers on column headers |
| defaultFilters | Record<string, FilterValue> | {} | Initial filter values keyed by field |
Columns
| Prop | Type | Default | Description |
| ---------------------- | ---------- | ----------------------- | ------------------------------------------------------ |
| enableColumnResize | boolean | true | Drag right edge of headers to resize |
| enableColumnReorder | boolean | true | Drag headers to reorder |
| enableColumnPinning | boolean | true | Allow pinning columns to the left |
| enableColumnHiding | boolean | true | Allow hiding columns via toolbar dropdown |
| enableColumnGroups | boolean | true (only when used) | Render parent header row for columns sharing a group |
| defaultHiddenColumns | string[] | [] | Field names hidden on initial render |
| defaultPinnedColumns | string[] | [] | Field names pinned to the left on initial render |
| minColumnWidth | number | 150 | Minimum column width in pixels (enforced on resize) |
Grouping
| Prop | Type | Default | Description |
| ---------------- | ---------------- | ------- | ---------------------------------- |
| enableGrouping | boolean | true | Allow grouping via the column menu |
| defaultGroupBy | string \| null | null | Initial group-by field |
Editing
| Prop | Type | Default | Description |
| --------------- | ------------------------------- | ------- | ----------------------------------------------------------------- |
| enableEditing | boolean | true | Allow inline editing (per-column editable: true still required) |
| onCellEdit | (e: CellEditEvent<T>) => void | — | Fires when a cell value is committed |
Row detail
| Prop | Type | Default | Description |
| ----------- | ----------------------- | ------- | ------------------------------------------------------------------------ |
| rowDetail | (row: T) => ReactNode | — | When provided, each row gets an expand chevron that reveals this content |
Total row
| Prop | Type | Default | Description |
| ---------------- | --------- | --------- | ---------------------------------- |
| showGrandTotal | boolean | true | Show the sticky grand-total row |
| totalLabel | string | "Total" | Label shown on the grand-total row |
Pagination
| Prop | Type | Default | Description |
| ------------------ | ---------- | -------------------------- | -------------------------------------------------------- |
| enablePagination | boolean | true | Show pagination bar (auto-hides when grouping is active) |
| defaultPageSize | number | 50 | Initial page size |
| pageSizeOptions | number[] | [20, 50, 100, 500, 1000] | Options in the page-size dropdown |
Clipboard
| Prop | Type | Default | Description |
| ------------ | --------- | ------- | ------------------------------------------------------------------------------------------- |
| enableCopy | boolean | true | Ctrl/Cmd+C copies the selected rows to the clipboard as TSV (pasteable into Excel / Sheets) |
Tooltips
| Prop | Type | Default | Description |
| -------------- | --------- | ------- | ---------------------------- |
| showTooltips | boolean | true | Globally toggle all tooltips |
| tooltipDelay | number | 350 | Hover delay in milliseconds |
Layout
| Prop | Type | Default | Description |
| ---------------- | ------------------ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| height | number \| string | 600 | Container height. A number is pixels; a CSS string ("100%", "100vh") is applied inline; a Tailwind class ("h-screen") is applied as className. Strings starting with h- or containing whitespace are treated as classes. |
| density | Density | — | Controlled density |
| defaultDensity | Density | "comfortable" | Initial density |
| className | string | — | Extra class on the grid root |
Events
| Prop | Type | Description |
| ------------------ | ----------------------------------------------------------------- | ------------------------------ |
| onRowClick | (row: T) => void | Fires on single-click of a row |
| onRowDoubleClick | (row: T) => void | Fires on double-click of a row |
| emptyState | ReactNode \| ((reason: "no-data" \| "no-results") => ReactNode) | Custom empty-state content |
Saved views
| Prop | Type | Description |
| --------------- | ----------------------------- | -------------------------------------------------------- |
| views | GridView[] | Controlled list of saved views (omit for internal state) |
| onViewsChange | (views: GridView[]) => void | Fires when views are added, deleted, or modified |
Search persistence
| Prop | Type | Description |
| --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| persistSearch | string | sessionStorage key. When set, the search-bar value is restored on mount and mirrored back on every change. Default: unset. |
ColumnDef
interface ColumnDef<T = Row> {
// Identity
field: keyof T & string;
header: string;
headerSubtitle?: string;
// Rendering
type?: "string" | "number" | "date" | "tag" | "custom";
width?: number;
minWidth?: number;
maxWidth?: number;
align?: "left" | "center" | "right";
render?: (value: any, row: T) => React.ReactNode;
valueGetter?: (row: T) => any;
cellClassName?: string | ((row: T) => string);
headerClassName?: string;
tagColorMap?: Record<string, TagStyle>; // for type: "tag"
// Per-column behavior overrides (defaults inherit from grid-level flags)
sortable?: boolean;
filterable?: boolean;
resizable?: boolean;
editable?: boolean;
hideable?: boolean;
pinnable?: boolean;
groupable?: boolean;
draggable?: boolean;
// Aggregation
aggregation?: "sum" | "avg" | "median" | "min" | "max" | "count";
aggFormat?: (value: number) => React.ReactNode;
// Visuals — toggled grid-wide by the toolbar "Visuals" menu
heatMap?:
| "green"
| "amber"
| "red"
| "blue"
| "violet"
| { from: string; to: string };
// Column groups — contiguous columns sharing this string render under a shared parent header
group?: string;
// Tooltips
headerTooltip?: React.ReactNode;
cellTooltip?: (value: any, row: T) => React.ReactNode;
}
interface TagStyle {
text: string; // CSS color or Tailwind class fragment
bg: string;
border?: string;
dot?: string;
}Event payloads
interface CellEditEvent<T = Row> {
rowId: string;
field: string;
oldValue: any;
newValue: any;
row: T;
}
interface SelectionEvent<T = Row> {
selectedRows: T[];
selectedIds: string[];
}Saved view shape
interface GridView {
name: string;
config: GridViewConfig;
}
interface GridViewConfig {
filters?: Record<string, FilterValue | null>;
sortConfig?: SortEntry[];
hidden?: string[];
pinned?: string[];
colOrder?: string[];
colWidths?: Record<string, number>;
density?: Density;
groupBy?: string | null;
expandedGroups?: string[];
pageSize?: number;
search?: string;
selected?: string[];
heatMapsOn?: boolean;
sparklinesOn?: boolean;
}Controlled mode
Today, only saved views are externally controllable — pass views and onViewsChange to manage them in your own state (useful when you want to persist views to a backend or sync across users):
const [views, setViews] = useState<GridView[]>(() => loadFromBackend());
useEffect(() => {
saveToBackend(views);
}, [views]);
<DataGrid
views={views}
onViewsChange={setViews}
columns={columns}
rows={rows}
/>;Filters, sort, selection, pagination, and edits are currently internal state. Server-side data, controlled filter/sort, and external selection sync aren't supported yet.
TypeScript
DataGrid is fully typed. The generic <T> flows through ColumnDef<T>, DataGridProps<T>, and every event payload — so render(value, row) knows the row shape, onCellEdit knows the field names, and onSelectionChange returns typed rows.
interface User {
id: string;
name: string;
mrr: number;
}
const columns: ColumnDef<User>[] = [
{ field: "name", header: "Name" }, // ✓ typed
{ field: "doesNotExist", header: "Nope" }, // ✗ TS error
{
field: "mrr",
header: "MRR",
render: (v, row) => `${row.name}: $${v}`, // row is typed as User
},
];
<DataGrid<User>
columns={columns}
rows={users}
onCellEdit={(e) => {
e.row; // typed as User
}}
/>;Accessibility
What works today: keyboard focus moves naturally through toolbar buttons via Tab, popovers (filter, column menu, density, export, views) are dismissable via Escape and click-outside, the select-all and per-row checkboxes are real <input type="checkbox"> elements with aria-labels, the expand chevron carries an aria-label, and Radix manages focus and ARIA on tooltips.
What's missing: no arrow-key navigation between cells, no role="grid" semantics on the table, no screen-reader testing has been done. The grid hasn't been through a formal a11y audit. If you're shipping to an enterprise that requires WCAG compliance, plan to test against your own checklist.
Browser support
Latest two versions of Chrome, Edge, Firefox, and Safari. Clipboard copy uses document.execCommand("copy") as a fallback so it works inside sandboxed iframes where the Clipboard API silently fails.
Development
This is part of a monorepo. To run locally:
git clone https://github.com/kxnyshk/data-grid.git
cd data-grid
npm install
npm run dev # starts the demo appEdits to packages/data-grid/src live-update the demo via the workspace symlink — no rebuild needed during dev.
Changelog
0.3.0
- Premium chip-rendering search bar
- Semantic CSS hooks (
kx-dg-*) for theming - Search hardening: strict equals (
=), numeric ranges, header-name lookup ?help popover with syntax referencepersistSearchprop for sessionStorage-backed search persistence- ⌘/ keyboard shortcut to focus search from anywhere
0.2.0
- Compiled CSS shipped — no Tailwind config required
0.1.0
- Initial release
License
MIT
