Introduction

Getting Started

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.

Our Motivation

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.

Why LyteNyte Grid Stands Out

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.

Core Edition vs. PRO Edition

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 Tag indicating the feature is only available for LyteNyte PRO icon next to the feature name on the navigation bar.

For a complete feature comparison between Core and PRO, check out our price page.

Quick Start Guide

In this guide, you will build a data table inspired by the log tables in Vercel and DataDog.

Getting Started Final Output

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.

Installing LyteNyte Grid

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
For Core:
pnpm add @1771technologies/lytenyte-core

Creating a Sized Container

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>;
}

Data and Columns

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.

Importing LyteNyte Grid

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>
  );
}

Basic Code Breakdown

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

Providing Data to the Grid

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

Styling the Grid

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";

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>

A Header With Sorting

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.

Custom Cell Renderers

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>
  );
}

The Row Detail Setup (Master Detail)

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.

Next Steps

Explore more LyteNyte Grid capabilities: