React invoice data grid
A virtualized invoice table with search, column filters, grouping, totals, an editable amount, copy and paste, and CSV/XLSX export.
Install
One command copies the files into your project, as code you then own. It needs a
components.json, and the shadcn registry page has the one-time setup.Terminal
npx shadcn@latest add https://www.box-kite.dev/r/data-grid.jsonLight and dark
The same component twice, each half in a theme of its own — nothing in it is written for either. Open it full screen to see it in yours.
light
Invoices
Search the table, filter a column, group by Team from a column menu, edit an amount, select a block of cells and copy it, or export what is on screen.
Invoices
6 of 6 shown
Reference
Customer
Team
Status
Amount
Issued
Select...
Select...
INV-2480
Ardent Supply
Northern
paid
$400
2026-01-01
INV-2481
Beacon Freight
Southern
pending
$537
2026-02-08
INV-2482
Corvus Analytics
Eastern
overdue
$674
2026-03-15
INV-2483
Delta Hardware
Western
paid
$811
2026-04-22
INV-2484
Evergreen Mills
Northern
pending
$948
2026-05-02
INV-2485
Fairline Media
Southern
overdue
$1,085
2026-06-09
INV-2486
Granite Foods
Eastern
paid
$1,222
2026-07-16
INV-2487
Harbour Logistics
Western
pending
$1,359
2026-08-23
INV-2488
Ardent Supply
Northern
overdue
$1,496
2026-09-03
INV-2489
Beacon Freight
Southern
paid
$1,633
2026-10-10
INV-2490
Corvus Analytics
Eastern
pending
$1,770
2026-11-17
INV-2491
Delta Hardware
Western
overdue
$1,907
2026-12-24
INV-2492
Evergreen Mills
Northern
paid
$2,044
2026-01-04
INV-2493
Fairline Media
Southern
pending
$2,181
2026-02-11
INV-2494
Granite Foods
Eastern
overdue
$2,318
2026-03-18
INV-2495
Harbour Logistics
Western
paid
$2,455
2026-04-25
INV-2496
Ardent Supply
Northern
pending
$2,592
2026-05-05
INV-2497
Beacon Freight
Southern
overdue
$2,729
2026-06-12
INV-2498
Corvus Analytics
Eastern
paid
$2,866
2026-07-19
INV-2499
Delta Hardware
Western
pending
$3,003
2026-08-26
INV-2500
Evergreen Mills
Northern
overdue
$3,140
2026-09-06
INV-2501
Fairline Media
Southern
paid
$3,277
2026-10-13
INV-2502
Granite Foods
Eastern
pending
$3,414
2026-11-20
INV-2503
Harbour Logistics
Western
overdue
$3,551
2026-12-27
INV-2504
Ardent Supply
Northern
paid
$3,688
2026-01-07
INV-2505
Beacon Freight
Southern
pending
$3,825
2026-02-14
INV-2506
Corvus Analytics
Eastern
overdue
$3,962
2026-03-21
INV-2507
Delta Hardware
Western
paid
$4,099
2026-04-01
INV-2508
Evergreen Mills
Northern
pending
$4,236
2026-05-08
INV-2509
Fairline Media
Southern
overdue
$4,373
2026-06-15
All invoices
200
$1,001,500
Rows: 200
Selected: 0
dark
Invoices
Search the table, filter a column, group by Team from a column menu, edit an amount, select a block of cells and copy it, or export what is on screen.
Invoices
6 of 6 shown
Reference
Customer
Team
Status
Amount
Issued
Select...
Select...
INV-2480
Ardent Supply
Northern
paid
$400
2026-01-01
INV-2481
Beacon Freight
Southern
pending
$537
2026-02-08
INV-2482
Corvus Analytics
Eastern
overdue
$674
2026-03-15
INV-2483
Delta Hardware
Western
paid
$811
2026-04-22
INV-2484
Evergreen Mills
Northern
pending
$948
2026-05-02
INV-2485
Fairline Media
Southern
overdue
$1,085
2026-06-09
INV-2486
Granite Foods
Eastern
paid
$1,222
2026-07-16
INV-2487
Harbour Logistics
Western
pending
$1,359
2026-08-23
INV-2488
Ardent Supply
Northern
overdue
$1,496
2026-09-03
INV-2489
Beacon Freight
Southern
paid
$1,633
2026-10-10
INV-2490
Corvus Analytics
Eastern
pending
$1,770
2026-11-17
INV-2491
Delta Hardware
Western
overdue
$1,907
2026-12-24
INV-2492
Evergreen Mills
Northern
paid
$2,044
2026-01-04
INV-2493
Fairline Media
Southern
pending
$2,181
2026-02-11
INV-2494
Granite Foods
Eastern
overdue
$2,318
2026-03-18
INV-2495
Harbour Logistics
Western
paid
$2,455
2026-04-25
INV-2496
Ardent Supply
Northern
pending
$2,592
2026-05-05
INV-2497
Beacon Freight
Southern
overdue
$2,729
2026-06-12
INV-2498
Corvus Analytics
Eastern
paid
$2,866
2026-07-19
INV-2499
Delta Hardware
Western
pending
$3,003
2026-08-26
INV-2500
Evergreen Mills
Northern
overdue
$3,140
2026-09-06
INV-2501
Fairline Media
Southern
paid
$3,277
2026-10-13
INV-2502
Granite Foods
Eastern
pending
$3,414
2026-11-20
INV-2503
Harbour Logistics
Western
overdue
$3,551
2026-12-27
INV-2504
Ardent Supply
Northern
paid
$3,688
2026-01-07
INV-2505
Beacon Freight
Southern
pending
$3,825
2026-02-14
INV-2506
Corvus Analytics
Eastern
overdue
$3,962
2026-03-21
INV-2507
Delta Hardware
Western
paid
$4,099
2026-04-01
INV-2508
Evergreen Mills
Northern
pending
$4,236
2026-05-08
INV-2509
Fairline Media
Southern
overdue
$4,373
2026-06-15
All invoices
200
$1,001,500
Rows: 200
Selected: 0
Source
The files the command writes, exactly as the preview above runs them. Every style is a prop, so there is no stylesheet to copy with them.
box-kite/invoices-grid.tsx
JSX
'use client';
import Box from '@box-kite/react';
import DataGrid, { type CellModel, type GridDefinition } from '@box-kite/react/components/dataGrid';
import Flex from '@box-kite/react/components/flex';
import { H2, P } from '@box-kite/react/components/semantics';
import { type Invoice, invoices } from './invoice-rows';
/**
* An invoices table with what a real one needs: search, per-column filters, grouping from the column
* menu, totals, an editable amount that is validated, a block of cells to copy, and CSV/XLSX export.
*
* None of it is configuration to keep in sync — the grid is one `def` object. Swap `invoices` for your
* own rows and edit the columns.
*/
const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
const STATUS_STYLES = {
paid: { bgColor: 'emerald-100', color: 'emerald-700', theme: { dark: { bgColor: 'emerald-950', color: 'emerald-300' } } },
pending: { bgColor: 'amber-100', color: 'amber-700', theme: { dark: { bgColor: 'amber-950', color: 'amber-300' } } },
overdue: { bgColor: 'rose-100', color: 'rose-700', theme: { dark: { bgColor: 'rose-950', color: 'rose-300' } } },
} as const;
// A cell renderer is a component defined outside the render: a new identity on every render remounts
// the whole column on every scroll.
function StatusCell({ cell }: { cell: CellModel<Invoice> }) {
const status = cell.value as Invoice['status'];
return (
<Flex ai="center" px={3} height="fit">
<Box px={2} py={0.5} borderRadius={4} fontSize={12} fontWeight={500} textTransform="capitalize" {...STATUS_STYLES[status]}>
{status}
</Box>
</Flex>
);
}
// `fontVariantNumeric` has no prop of its own, so it goes through `css` — still one shared class, not
// a style attribute.
function AmountCell({ cell }: { cell: CellModel<Invoice> }) {
return (
<Flex ai="center" jc="end" px={3} height="fit" css={{ fontVariantNumeric: 'tabular-nums' }}>
{money.format(cell.value as number)}
</Flex>
);
}
const definition: GridDefinition<Invoice> = {
rowKey: 'id',
title: 'Invoices',
topBar: true,
bottomBar: true,
globalFilter: true,
rowSelection: { pinned: true },
rangeSelection: true,
footer: { label: 'All invoices' },
export: { fileName: 'invoices' },
visibleRowsCount: 12,
rowHeight: 44,
columns: [
{ key: 'reference', header: 'Reference', width: 120, filterable: true },
{ key: 'customer', header: 'Customer', width: 170, filterable: true },
{ key: 'team', header: 'Team', width: 130, filterable: { type: 'multiselect' } },
{ key: 'status', header: 'Status', width: 120, filterable: { type: 'multiselect' }, Cell: StatusCell, aggregate: 'count' },
{
key: 'amount',
header: 'Amount',
width: 130,
align: 'end',
editable: true,
filterable: { type: 'number' },
aggregate: 'sum',
Cell: AmountCell,
AggregateCell: ({ cell }) => (
<Flex ai="center" jc="end" px={3} height="fit" fontWeight={600} css={{ fontVariantNumeric: 'tabular-nums' }}>
{cell.value === null ? '' : money.format(Number(cell.value))}
</Flex>
),
// An export runs no React, so the column says what it writes rather than exporting the renderer.
exportValue: (row) => row.amount,
exportFormat: '#,##0',
},
{ key: 'issued', header: 'Issued', width: 120 },
],
// One function both judges an edit and is told about it: a string is a refusal, and the message is
// what the cell shows.
onCellEdit: ({ value }) => {
const amount = Number(value);
if (!Number.isFinite(amount) || amount <= 0) return 'An invoice is worth more than nothing.';
if (amount > 100_000) return 'Anything over $100,000 needs an approval.';
},
};
export default function InvoicesGrid() {
return (
<Flex d="column" gap={4}>
<Box>
<H2 fontSize={20} fontWeight={600}>
Invoices
</H2>
<P mt={1} fontSize={14} color="slate-600" theme={{ dark: { color: 'slate-400' } }}>
Search the table, filter a column, group by Team from a column menu, edit an amount, select a block of cells and copy it, or
export what is on screen.
</P>
</Box>
<DataGrid data={invoices} def={definition} />
</Flex>
);
}
box-kite/invoice-rows.ts
JSX
/** The rows the block is wired to. Replace this file with your own fetch — the grid reads plain objects. */
export interface Invoice {
id: number;
reference: string;
customer: string;
team: string;
status: 'paid' | 'pending' | 'overdue';
amount: number;
issued: string;
}
const TEAMS = ['Northern', 'Southern', 'Eastern', 'Western'];
const STATUSES = ['paid', 'pending', 'overdue'] as const;
const CUSTOMERS = [
'Ardent Supply',
'Beacon Freight',
'Corvus Analytics',
'Delta Hardware',
'Evergreen Mills',
'Fairline Media',
'Granite Foods',
'Harbour Logistics',
];
/**
* Two hundred invoices generated from the index, so the block has something to sort, filter, group and
* total without shipping a data file — and every reload shows the same table.
*/
export const invoices: Invoice[] = Array.from({ length: 200 }, (_, index) => ({
id: index + 1,
reference: `INV-${(2480 + index).toString()}`,
customer: CUSTOMERS[index % CUSTOMERS.length],
team: TEAMS[index % TEAMS.length],
status: STATUSES[index % STATUSES.length],
amount: 400 + ((index * 137) % 9600),
issued: new Date(Date.UTC(2026, index % 12, ((index * 7) % 27) + 1)).toISOString().slice(0, 10),
}));