Get started with LyteNyte Grid, a modern React data grid designed for enterprise-scale data challenges. Built in React, for React, it enables developers to ship faster and more efficiently than ever before.
No wrappers. No dependencies. Open code.
Before LyteNyte Grid, we were trapped in a cycle of frustration with bloated, brittle, and bizarrely over-engineered data grid libraries. Every new project became a ritual of fighting APIs that felt like they were written by a committee that never used React.
Here's what we kept running into again and again:
Customization was a nightmare. Clunky, opaque APIs made even basic tweaks feel like defusing a bomb. Two-way data bindings, sync issues between React and the grid... we've seen things.
Server data loading was a disaster. Optimistic updates, partial rendering, and caching never worked properly, or worse, worked sometimes, which is somehow more dangerous.
Performance collapsed under pressure. Beyond trivial datasets, most grids fell apart. Virtualization failed. Re-renders multiplied. Main thread got blocked. Users rage-quit.
Breaking changes broke more than code. New versions came with surprises, and not the fun kind. We were refactoring the same grid logic every quarter just to stay afloat.
Styling was their way or no way. We were forced to adopt unfamiliar styling systems just to make things look half-decent, adding yet another layer of complexity.
Bundle sizes were obscene. Grid libraries ballooned app load times by 1-3 seconds. That's not just technical debt; it's user abandonment in disguise.
So… We patched, duct-taped, forked, and cursed. Over time, our quick fixes turned into long-term liabilities. Technical debt grew. Dev velocity dropped. Maintenance costs soared. All because the tools we relied on couldn't keep up.
We built LyteNyte Grid to end that cycle.
At the heart of LyteNyte Grid is a commitment to the developer and user experience based on the principle of 'seamless simplicity.'
Here's why we stand out:
⚛️ Clean, Declarative API: LyteNyte Grid exposes a minimal, declarative API aligned with React's data flow and state model. No wrappers, no adapter layers. No awkward integrations, only cleaner, more maintainable code.
📦 Tiny Bundle Size: Core edition is a tiny 36kb gzipped, while the PRO edition weighs only 49kb gzipped, so you no longer have to choose between advanced functionality and a fast user experience.
⚡ Unrivaled Speed: LyteNyte can handle 10,000+ updates per second and render millions of rows. Our reactive state architecture means performance doesn't degrade when paginating, filtering, or pulling from the server.
🧩 Headless by Design, Components Included: An industry first. Ultimate flexibility to choose between our pre-styled themes or drop into full headless mode for 100% control. Covering any use case you may have for a data table.
🏢 Feature-Rich, Enterprise Ready: Handles the most demanding workflows with a comprehensive feature set that includes pivot tables, tree data, server-side loading, custom cell rendering, rich cell editing, and more, all from a single package, giving you one consistent API to build with.
🫶 Simple Licensing, Transparent Support: Straightforward licensing that won't leave you guessing what's permissible. All support is handled publicly on GitHub, giving you complete transparency into our response times.
LyteNyte Grid is available in two editions: Core and PRO.
LyteNyte Grid PRO is built on top of LyteNyte Grid Core, meaning it includes all Core features plus additional advanced capabilities for the most demanding enterprise use cases. This architecture ensures a seamless upgrade path; you can start with Core and switch to PRO later without refactoring, as it's a non-breaking, drop-in replacement.
LyteNyte Core Edition: Free, open source (Apache 2.0), and genuinely useful. Includes essential features such as sorting, filtering, row grouping, column auto-sizing, detail views, data exporting, and others.
LyteNyte Grid PRO Edition: A commercial edition (EULA) with advanced capabilities like server data loading, column and filter manager components, tree data, column pivoting, and more sophisticated data table tools.
To determine if a feature is exclusively part of the PRO edition,
look for the icon next to the feature name on the navigation bar.
For a complete feature comparison between Core and PRO, check out our price page.
In this guide, you will build a data table inspired by the log tables in Vercel and DataDog.
This demo shows the final output of the guide. If you prefer to jump straight to the complete code, fork the working demo by clicking the StackBlitz or Code Sandbox icon under the code frame.
This guide works with either edition of LyteNyte Grid. If you have a license, install PRO. You can use PRO without a license, but the page will show a watermark.
pnpm add @1771technologies/lytenyte-pro
pnpm add @1771technologies/lytenyte-core
If you do not have a React project yet, we recommend using Vite. Create a project quickly with:
pnpm create vite
For details, see the Vite getting started docs.
LyteNyte Grid uses virtualization for maximum performance. It virtualizes both rows and columns. This requires a sized container - a DOM element that occupies space even without child nodes.
The simplest approach is to set the height
style on the element, which we do
here. For more on sized containers, see the
Sized Container guide.
Define a sized container for LyteNyte Grid:
export function GettingStartedDemo() {
return <div style={{ width: "100%", height: "400px" }}></div>;
}
Next, import LyteNyteGrid
, prepare your data, and define columns that tell the
grid what to display.
This demo uses sample request log data. Here is one item:
{
"Date": "2025-08-01 10:12:04",
"Status": 200,
"Method": "GET",
"Pathname": "/",
"Latency": 51,
"region.shortname": "sin",
"region.fullname": "Singapore",
"timing-phase.dns": 0,
"timing-phase.tls": 10,
"timing-phase.ttfb": 9,
"timing-phase.connection": 23,
"timing-phase.transfer": 9
}
You can download the full data file from our GitHub example.
LyteNyte Grid uses a modular design to minimize bundle size. It exposes named exports to maximize tree-shaking.
The main export is the Grid
object, which contains components and hooks for
building a grid.
Grid
also exposes the useLyteNyte
hook. It resembles React's useState
:
the provided value initializes state, and later changes to that object do not
update the initial value.
The code below shows the minimal setup for this demo. Paste it into your editor. We will enhance it as we progress.
"use client";
import { Grid } from "@1771technologies/lytenyte-pro";
import type { Column, RowLayout } from "@1771technologies/lytenyte-pro/types";
import { memo, useId } from "react";
type RequestData = {
Date: string;
Status: number;
Method: string;
Pathname: string;
Latency: number;
"region.shortname": string;
"region.fullname": string;
"timing-phase.dns": number;
"timing-phase.tls": number;
"timing-phase.ttfb": number;
"timing-phase.connection": number;
"timing-phase.transfer": number;
};
const columns: Column<RequestData>[] = [
{ id: "Date", name: "Date", type: "datetime" },
{ id: "Status", name: "Status" },
{ id: "Method", name: "Method" },
{ id: "timing-phase", name: "Timing Phase" },
{ id: "Pathname", name: "Pathname" },
{ id: "Latency", name: "Latency" },
{ id: "region", name: "Region" },
];
export function GettingStartedDemo() {
const grid = Grid.useLyteNyte({
gridId: useId(),
columns,
});
const view = grid.view.useValue();
return (
<div style={{ width: "100%", height: "400px" }}>
<Grid.Root grid={grid}>
<Grid.Viewport>
<Grid.Header>
{view.header.layout.map((row, i) => (
<Grid.HeaderRow headerRowIndex={i} key={i}>
{row.map((c) => {
if (c.kind === "group") {
return (
<Grid.HeaderGroupCell cell={c} key={c.idOccurrence} />
);
}
return <Grid.HeaderCell cell={c} key={c.column.id} />;
})}
</Grid.HeaderRow>
))}
</Grid.Header>
<Grid.RowsContainer>
<Grid.RowsCenter>
{view.rows.center.map((row) => {
if (row.kind === "full-width") {
return <Grid.RowFullWidth row={row} key={row.id} />;
}
return (
<Grid.Row key={row.id} row={row} accepted={["row"]}>
{row.cells.map((cell) => (
<Grid.Cell cell={cell} key={cell.id} />
))}
</Grid.Row>
);
})}
</Grid.RowsCenter>
</Grid.RowsContainer>
</Grid.Viewport>
</Grid.Root>
</div>
);
}
The code starts with:
"use client";
Use this directive when you combine LyteNyte Grid with React Server Components. If you build a simple SPA, you can omit it.
Next, the imports:
import { Grid } from "@1771technologies/lytenyte-pro";
import type { Column, RowLayout } from "@1771technologies/lytenyte-pro/types";
import { memo, useId } from "react";
Grid
contains the headless building blocks of LyteNyte Grid. The types package
exposes helpful types to keep your app type-safe.
Then, define columns:
const columns: Column<RequestData>[] = [
{ id: "Date", name: "Date", type: "datetime" },
{ id: "Status", name: "Status" },
{ id: "Method", name: "Method" },
{ id: "timing-phase", name: "Timing Phase" },
{ id: "Pathname", name: "Pathname" },
{ id: "Latency", name: "Latency" },
{ id: "region", name: "Region" },
];
Each column must have a unique id
. Other fields are optional, but they improve
the experience.
Finally, the grid component:
<div style={{ width: "100%", height: "400px" }}>
<Grid.Root grid={grid}>{/* Omitted for brevity */}</Grid.Root>
</div>
The Grid
object provides components that make up LyteNyte Grid. The general
structure looks like:
Viewport
Header
HeaderRow
HeaderCell, HeaderCell, HeaderCell
RowsContainer
RowsCenter
Row
Cell, Cell, Cell
When you use column groups, HeaderCell
may be a HeaderGroupCell
, and there
may be multiple HeaderRow
s. Rows can also be full width. The template above
handles these cases.
RowsContainer
renders RowsCenter
. The grid also supports RowsTop
and
RowsBottom
for pinned rows. See the Row Pinning guide
for details.
LyteNyte Grid reads data from a row data source. The most common option is a client-side data source when all data is available in the browser.
Import useClientRowDataSource
and provide the requestData
sample data:
import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";
// Rest omitted
export function MyLyteNyteGridComponent() {
const ds = useClientRowDataSource<RequestData>({
data: requestData,
});
const grid = Grid.useLyteNyte({
gridId: useId(),
columns,
rowDataSource: ds,
});
// Omitted ...
}
By default, LyteNyte Grid is unstyled. Only minimal styles for core behavior are included.
You can style it yourself or use a built-in theme. You can also combine both. The examples below use Tailwind CSS.
Import the Grid CSS:
import "@1771technologies/lytenyte-pro/grid.css";
We use Tailwind CSS with these color variables in the Tailwind config:
{
colors: {
"ln-gray-00": "var(--lng1771-gray-00)",
"ln-gray-02": "var(--lng1771-gray-02)",
"ln-gray-05": "var(--lng1771-gray-05)",
"ln-gray-10": "var(--lng1771-gray-10)",
"ln-gray-20": "var(--lng1771-gray-20)",
"ln-gray-30": "var(--lng1771-gray-30)",
"ln-gray-40": "var(--lng1771-gray-40)",
"ln-gray-50": "var(--lng1771-gray-50)",
"ln-gray-60": "var(--lng1771-gray-60)",
"ln-gray-70": "var(--lng1771-gray-70)",
"ln-gray-80": "var(--lng1771-gray-80)",
"ln-gray-90": "var(--lng1771-gray-90)",
"ln-gray-100": "var(--lng1771-gray-100)",
"ln-primary-05": "var(--lng1771-primary-05)",
"ln-primary-10": "var(--lng1771-primary-10)",
"ln-primary-30": "var(--lng1771-primary-30)",
"ln-primary-50": "var(--lng1771-primary-50)",
"ln-primary-70": "var(--lng1771-primary-70)",
"ln-primary-90": "var(--lng1771-primary-90)",
"ln-focus-outline": "var(--lng1771-focus-outline)"
}
}
After importing the CSS, add the lng-grid
class to a container to apply the
theme. You can also add one of these color themes:
light
for light mode.dark
for dark mode.lng1771-teal
for a sleek dark teal theme.lng1771-term256
for a minimal monospaced dark theme.<div className="lng-grid" style={{ width: "100%", height: "400px" }}>
<Grid.Root grid={grid}>{/* ... */}</Grid.Root>
</div>
Let's improve usability by adding sortable headers. Set a headerRenderer
on
all columns using columnBase
:
const grid = Grid.useLyteNyte({
gridId: useId(),
columns,
columnBase: {
headerRenderer: Header,
},
});
Now define the Header
renderer:
export function Header({
column,
grid,
}: HeaderCellRendererParams<RequestData>) {
const sort = grid.state.sortModel
.useValue()
.find((c) => c.columnId === column.id);
const isDescending = sort?.isDescending ?? false;
return (
<div
className="flex items-center px-2 w-full h-full text-sm
bg-ln-gray-05 hover:bg-ln-gray-10 transition-all"
onClick={() => {
const current = grid.api.sortForColumn(column.id);
if (current == null) {
let sort: SortModelItem<RequestData>;
const columnId = column.id;
if (customComparators[column.id]) {
sort = {
columnId,
sort: {
kind: "custom",
columnId,
comparator: customComparators[column.id],
},
};
} else if (column.type === "datetime") {
sort = {
columnId,
sort: {
kind: "date",
options: { includeTime: true },
},
};
} else if (column.type === "number") {
sort = { columnId, sort: { kind: "number" } };
} else {
sort = { columnId, sort: { kind: "string" } };
}
grid.state.sortModel.set([sort]);
return;
}
if (!current.sort.isDescending) {
grid.state.sortModel.set([{ ...current.sort, isDescending: true }]);
} else {
grid.state.sortModel.set([]);
}
}}
>
{column.name ?? column.id}
{sort && (
<>
{!isDescending ? (
<ArrowUpIcon className="size-4" />
) : (
<ArrowDownIcon className="size-4" />
)}
</>
)}
</div>
);
}
This cycles through ascending → descending → none. It also supports custom comparators keyed by column id:
const customComparators: Record<string, SortComparatorFn<RequestData>> = {
region: (left, right) => {
if (left.kind === "branch" || right.kind === "branch") {
if (left.kind === "branch" && right.kind === "branch") return 0;
if (left.kind === "branch" && right.kind !== "branch") return -1;
if (left.kind !== "branch" && right.kind === "branch") return 1;
}
if (!left.data || !right.data) return !left.data ? 1 : -1;
const leftData = left.data as RequestData;
const rightData = right.data as RequestData;
return leftData["region.fullname"].localeCompare(
rightData["region.fullname"]
);
},
"timing-phase": (left, right) => {
if (left.kind === "branch" || right.kind === "branch") {
if (left.kind === "branch" && right.kind === "branch") return 0;
if (left.kind === "branch" && right.kind !== "branch") return -1;
if (left.kind !== "branch" && right.kind === "branch") return 1;
}
if (!left.data || !right.data) return !left.data ? 1 : -1;
const leftData = left.data as RequestData;
const rightData = right.data as RequestData;
return leftData.Latency - rightData.Latency;
},
};
We use a custom comparator for region
and timing-phase
because those fields
store objects. Basic string or number sorts do not work there.
Next, add cell renderers to improve readability. You can pass a cellRenderer
to a column. Below we add a renderer for the Date
column. See our
GitHub components file
for more renderers.
Declare the renderer on the column:
const columns: Column<RequestData>[] = [
{
id: "Date",
name: "Date",
cellRenderer: DateCell,
type: "datetime",
},
// other columns...
];
Then define DateCell
. After editing column definitions, refresh the page since
hot reload does not apply to column metadata:
export function DateCell({
column,
row,
grid,
}: CellRendererParams<RequestData>) {
const field = grid.api.columnField(column, row);
const niceDate = useMemo(() => {
if (typeof field !== "string") return null;
return format(field, "MMM dd, yyyy HH:mm:ss");
}, [field]);
if (!niceDate) return null;
return (
<div
className="flex items-center px-2 h-full w-full
text-ln-gray-70 font-light font-mono"
>
{niceDate}
</div>
);
}
LyteNyte Grid supports master-detail out of the box. Define a detail renderer and a way to expand or collapse detail rows.
Enable row detail in the grid:
const grid = Grid.useLyteNyte({
gridId: useId(),
columns,
rowDetailHeight: 200,
rowDetailRenderer: RowDetailRenderer,
// other properties...
});
Then implement the detail component:
export function RowDetailRenderer({
row,
grid,
}: RowDetailRendererParams<RequestData>) {
if (!grid.api.rowIsLeaf(row) || !row.data) return null;
return (
<div className="w-full h-full p-3">{/* Detail renderer content */}</div>
);
}
To toggle details, use the marker column - a fixed column at the start of the grid that you can use for auxiliary controls:
const grid = Grid.useLyteNyte({
gridId: useId(),
columns,
rowDetailHeight: 200,
rowDetailRenderer: RowDetailRenderer,
columnMarkerEnabled: true,
columnMarker: {
cellRenderer: ({ row, grid }) => {
const isExpanded = grid.api.rowDetailIsExpanded(row);
return (
<button
className="flex items-center justify-center h-full w-full"
onClick={() => grid.api.rowDetailToggle(row)}
>
{isExpanded ? <ChevronDownIcon /> : <ChevronRightIcon />}
</button>
);
},
},
// other properties...
});
With these changes, the grid supports row details. You can extend this pattern as needed.
Explore more LyteNyte Grid capabilities: