Introduction
Getting Started Copy Page Open additional page export options
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 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 and state sync issues between React and the grid… we’ve seen it all .
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, sometimes longer.
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 30kb gzipped , while the PRO edition weighs
only 40kb 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 options. All support is
handled publicly on GitHub. You get 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, so it includes all Core
features plus 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, since 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 pricing page .
In this guide, you will build a data table inspired by the log tables in Vercel and DataDog.
Getting Started import " @1771technologies/lytenyte-pro/light-dark.css " ;
type UseClientDataSourceParams ,
} from " @1771technologies/lytenyte-pro " ;
import type { RequestData } from " ./data.js " ;
import { requestData } from " ./data.js " ;
} from " ./components.js " ;
import { useMemo , useState } from " react " ;
import { sortComparators } from " ./comparators.js " ;
export interface GridSpec {
column : { sort ?: " asc " | " desc " | null };
const base : Grid . Props < GridSpec > [ " columnBase " ] = {
const marker : Grid . Props < GridSpec > [ " columnMarker " ] = {
headerRenderer : () => < div className = " sr-only " >Toggle row detail expansion</ div >,
cellRenderer : MarkerCell ,
export default function GettingStartedDemo () {
const [ columns , setColumns ] = useState < Grid . Column < GridSpec > [] > ([
{ id : " Date " , name : " Date " , width : 200 , type : " datetime " , cellRenderer : DateCell },
{ id : " Status " , name : " Status " , width : 100 , cellRenderer : StatusCell },
{ id : " Method " , name : " Method " , width : 100 , cellRenderer : MethodCell },
{ id : " timing-phase " , name : " Timing Phase " , cellRenderer : TimingPhaseCell },
{ id : " Pathname " , name : " Pathname " , cellRenderer : PathnameCell },
{ id : " Latency " , name : " Latency " , width : 120 , type : " number " , cellRenderer : LatencyCell },
{ id : " region " , name : " Region " , cellRenderer : RegionCell },
const sort = useMemo < UseClientDataSourceParams < GridSpec > [ " sort " ] > ( () => {
const colWithSort = columns . find ( ( x ) => x . sort ) ;
if ( ! colWithSort ) return null ;
if ( sortComparators [ colWithSort . id ])
return [ { dim : sortComparators [ colWithSort . id ] , descending : colWithSort . sort === " desc " } ] ;
return [ { dim : colWithSort , descending : colWithSort . sort === " desc " } ] ;
const ds = useClientDataSource < GridSpec > ( {
< div className = " demo ln-grid " style = {{ height: 400 }}>
onColumnsChange = { setColumns }
rowDetailRenderer = { RowDetailRenderer }
import type { GridSpec } from " ./demo " ;
import { useMemo } from " react " ;
import { format } from " date-fns " ;
import type { RequestData } from " ./data " ;
import { PieChart } from " react-minimal-pie-chart " ;
import { ArrowDownIcon , ArrowUpIcon , ChevronDownIcon , ChevronRightIcon } from " @radix-ui/react-icons " ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
export function Header ({ api , column } : Grid . T . HeaderParams < GridSpec >) {
className = " text-ln-gray-60 group relative flex h-full w-full cursor-pointer items-center px-1 text-sm transition-colors "
const columns = api . props (). columns ;
const updates : Record < string , Partial < Grid . Column < GridSpec >>> = {};
const columnWithSort = columns . filter (( x ) => x . sort );
columnWithSort . forEach (( x ) => {
updates [ x . id ] = { sort: null };
if ( column . sort === " asc " ) {
updates [ column . id ] = { sort: null };
} else if ( column . sort === " desc " ) {
updates [ column . id ] = { sort: " asc " };
updates [ column . id ] = { sort: " desc " };
api . columnUpdate ( updates );
< div className = " sort-button flex w-full items-center justify-between rounded px-1 py-1 transition-colors " >
{ column . name ?? column . id }
{ column . sort === " asc " && < ArrowUpIcon className = " text-ln-text-dark size-4 " />}
{ column . sort === " desc " && < ArrowDownIcon className = " text-ln-text-dark size-4 " />}
export function MarkerCell ({ detailExpanded , row , api } : Grid . T . CellRendererParams < GridSpec >) {
className = " text-ln-text flex h-full w-[calc(100%-1px)] cursor-pointer items-center justify-center "
onClick = {() => api . rowDetailToggle ( row )}
< ChevronDownIcon width = { 20 } height = { 20 } />
< ChevronRightIcon width = { 20 } height = { 20 } />
export function DateCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const niceDate = useMemo ( () => {
if ( typeof field !== " string " ) return null ;
return format ( field , " MMM dd, yyyy HH:mm:ss " ) ;
// Guard against bad values and render nothing
if ( ! niceDate ) return null ;
return < div className = " text-ln-text flex h-full w-full items-center tabular-nums " >{ niceDate }</ div >;
export function StatusCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const status = api . columnField ( column , row ) ;
// Guard against bad values
if ( typeof status !== " number " ) return null ;
< div className = { clsx ( " flex h-full w-full items-center text-xs font-bold " )}>
status < 400 && " text-ln-primary-50 bg-[#126CFF1F] " ,
status >= 400 && status < 500 && " bg-[#FF991D1C] text-[#EEA760] " ,
status >= 500 && " bg-[#e63d3d2d] text-[#e63d3d] " ,
export function MethodCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const method = api . columnField ( column , row ) ;
// Guard against bad values
if ( typeof method !== " string " ) return null ;
< div className = { clsx ( " flex h-full w-full items-center text-xs font-bold " )}>
method === " GET " && " text-ln-primary-50 bg-[#126CFF1F] " ,
( method === " PATCH " || method === " PUT " || method === " POST " ) && " bg-[#FF991D1C] text-[#EEA760] " ,
method === " DELETE " && " bg-[#e63d3d2d] text-[#e63d3d] " ,
export function PathnameCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const path = api . columnField ( column , row ) ;
if ( typeof path !== " string " ) return null ;
< div className = " text-ln-text-dark flex h-full w-full items-center text-sm " >
< div className = " text-ln-primary-50 w-full overflow-hidden text-ellipsis text-nowrap " >{ path }</ div >
const numberFormatter = new Intl . NumberFormat ( " en-Us " , {
maximumFractionDigits : 0 ,
minimumFractionDigits : 0 ,
export function LatencyCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const ms = api . columnField ( column , row ) ;
if ( typeof ms !== " number " ) return null ;
< div className = " text-ln-text-dark flex h-full w-full items-center text-sm tabular-nums " >
< span className = " text-ln-gray-100 " >{ numberFormatter . format ( ms )}</ span >
< span className = " text-ln-text-light text-xs " >ms</ span >
export function RegionCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
// Only render for leaf rows and we have some data
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const shortName = row . data [ " region.shortname " ] ;
const longName = row . data [ " region.fullname " ] ;
< div className = " flex h-full w-full items-center " >
< div className = " flex items-baseline gap-2 text-sm " >
< div className = " text-ln-gray-100 " >{ shortName }</ div >
< div className = " text-ln-text-light leading-4 " >{ longName }</ div >
const colors = [ " var(--transfer) " , " var(--dns) " , " var(--connection) " , " var(--ttfb) " , " var(--tls) " ] ;
export function TimingPhaseCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
// Guard against rows that are not leafs or rows that have no data.
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
row . data [ " timing-phase.connection " ] +
row . data [ " timing-phase.dns " ] +
row . data [ " timing-phase.tls " ] +
row . data [ " timing-phase.transfer " ] +
row . data [ " timing-phase.ttfb " ] ;
const connectionPer = ( row . data [ " timing-phase.connection " ] / total ) * 100 ;
const dnsPer = ( row . data [ " timing-phase.dns " ] / total ) * 100 ;
const tlPer = ( row . data [ " timing-phase.tls " ] / total ) * 100 ;
const transferPer = ( row . data [ " timing-phase.transfer " ] / total ) * 100 ;
const ttfbPer = ( row . data [ " timing-phase.ttfb " ] / total ) * 100 ;
const values = [ connectionPer , dnsPer , tlPer , transferPer , ttfbPer ] ;
< div className = " flex h-full w-full items-center " >
< div className = " flex h-4 w-full items-center gap-px overflow-hidden " >
style = {{ width: `${ v } % ` , background: colors [ i ] }}
className = { clsx ( " h-full rounded-sm " )}
export function RowDetailRenderer ({ row , api } : Grid . T . RowParams < GridSpec >) {
// Guard against empty data.
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
row . data [ " timing-phase.connection " ] +
row . data [ " timing-phase.dns " ] +
row . data [ " timing-phase.tls " ] +
row . data [ " timing-phase.transfer " ] +
row . data [ " timing-phase.ttfb " ] ;
const connectionPer = ( row . data [ " timing-phase.connection " ] / total ) * 100 ;
const dnsPer = ( row . data [ " timing-phase.dns " ] / total ) * 100 ;
const tlPer = ( row . data [ " timing-phase.tls " ] / total ) * 100 ;
const transferPer = ( row . data [ " timing-phase.transfer " ] / total ) * 100 ;
const ttfbPer = ( row . data [ " timing-phase.ttfb " ] / total ) * 100 ;
< div className = " pt-1.75 flex h-full flex-col px-4 pb-5 text-sm " >
< h3 className = " text-ln-text-xlight mt-0 text-xs font-medium " >Timing Phases</ h3 >
< div className = " flex flex-1 gap-2 pt-1.5 " >
< div className = " bg-ln-gray-00 border-ln-gray-20 h-full flex-1 rounded-[10px] border " >
< div className = " grid-cols[auto_auto_1fr] grid grid-rows-5 gap-1 gap-x-4 p-4 md:grid-cols-[auto_auto_200px_auto] " >
msPercentage = { transferPer }
msValue = { row . data [ " timing-phase.transfer " ]}
msValue = { row . data [ " timing-phase.dns " ]}
msPercentage = { connectionPer }
msValue = { row . data [ " timing-phase.connection " ]}
msValue = { row . data [ " timing-phase.ttfb " ]}
msValue = { row . data [ " timing-phase.tls " ]}
< div className = " col-start-3 row-span-full flex h-full flex-1 items-center justify-center " >
< TimingPhasePieChart row = { row . data } />
interface TimePhaseRowProps {
readonly msValue : number ;
readonly msPercentage : number ;
function TimingPhaseRow ({ color , msValue , msPercentage , label } : TimePhaseRowProps ) {
< div className = " text-sm " >{ label }</ div >
< div className = " text-sm tabular-nums " >{ msPercentage . toFixed ( 2 )}%</ div >
< div className = " col-start-4 hidden items-center justify-end gap-1 text-sm md:flex " >
< span className = " text-ln-gray-100 " >{ numberFormatter . format ( msValue )}</ span >
< span className = " text-ln-text-xlight text-xs " >ms</ span >
function TimingPhasePieChart ({ row } : { row : RequestData }) {
const data = useMemo ( () => {
{ subject : " Transfer " , value : row [ " timing-phase.transfer " ] , color : colors [ 0 ] },
{ subject : " DNS " , value : row [ " timing-phase.dns " ] , color : colors [ 1 ] },
{ subject : " Connection " , value : row [ " timing-phase.connection " ] , color : colors [ 2 ] },
{ subject : " TTFB " , value : row [ " timing-phase.ttfb " ] , color : colors [ 3 ] },
{ subject : " TLS " , value : row [ " timing-phase.tls " ] , color : colors [ 4 ] },
< div style = {{ height: 100 }}>
< PieChart data = { data } startAngle = { 180 } lengthAngle = { 180 } center = {[ 50 , 75 ]} paddingAngle = { 1 } />
import type { GridSpec } from " ./demo.js " ;
import type { RequestData } from " ./data.js " ;
import { compareAsc } from " date-fns " ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
export const sortComparators : Record < string , Grid . T . SortFn < GridSpec [ " data " ] >> = {
region : ( left , right ) => {
if ( left . kind !== " leaf " && right . kind !== " leaf " ) return 0 ;
if ( left . kind === " leaf " && right . kind !== " leaf " ) return - 1 ;
if ( left . kind !== " leaf " && right . kind === " leaf " ) return 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 !== " leaf " && right . kind !== " leaf " ) return 0 ;
if ( left . kind === " leaf " && right . kind !== " leaf " ) return - 1 ;
if ( left . kind !== " leaf " && right . kind === " leaf " ) return 1 ;
const leftData = left . data as RequestData ;
const rightData = right . data as RequestData ;
return leftData . Latency - rightData . Latency ;
if ( left . kind !== " leaf " && right . kind !== " leaf " ) return 0 ;
if ( left . kind === " leaf " && right . kind !== " leaf " ) return - 1 ;
if ( left . kind !== " leaf " && right . kind === " leaf " ) return 1 ;
const leftData = left . data as RequestData ;
const rightData = right . data as RequestData ;
return compareAsc ( leftData . Date , rightData . Date ) ;
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.
npm install @1771technologies/lytenyte-pro
pnpm add @1771technologies/lytenyte-pro
yarn add @1771technologies/lytenyte-pro
bun add @1771technologies/lytenyte-pro
For Core:
npm install @1771technologies/lytenyte-core
pnpm add @1771technologies/lytenyte-core
yarn add @1771technologies/lytenyte-core
bun add @1771technologies/lytenyte-core
LyteNyte Grid uses a modular design to minimize bundle size. The library exposes named
exports to maximize tree-shaking.
Grid is the main export. Grid is a React component, and it includes additional component
functions attached to it. In this guide, you only need the Grid function.
See the Headless Component Parts guide for advanced usage of the
different component parts that make up LyteNyte Grid.
Start by:
Importing the Grid function.
Defining the grid columns.
The code below shows each part. The rest of this guide builds on this code,
so if you are following along in your editor, copy it.
import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { Grid } from " @1771technologies/lytenyte-pro " ;
const columns : Grid . Column < GridSpec > [] = [
{ id : " Date " , name : " Date " , width : 200 , type : " datetime " },
{ id : " Status " , name : " Status " , width : 100 },
{ id : " Method " , name : " Method " , width : 100 },
{ id : " timing-phase " , name : " Timing Phase " },
{ id : " Pathname " , name : " Pathname " },
{ id : " Latency " , name : " Latency " , width : 120 , type : " number " },
{ id : " region " , name : " Region " },
export default function GettingStarted () {
< div className = " ln-grid " style = {{ height: 400 }}>
< Grid columns = { columns } />
The annotated lines mark important parts:
Defines the set of column definitions. The grid uses these definitions to display content.
To learn more about the available properties, see the Column Overview guide .
Creates a 400px-tall container for the grid. LyteNyte Grid virtualizes rows by default,
so the grid needs a container with a height to fill. See the Responsive Container guide
for different approaches to creating a grid container. The container also applies the ln-grid class.
LyteNyte Grid requires the ln-grid class for the pre-made styles. To learn more about styling, see
the Grid Theming guide .
Renders the grid. This example uses the default Grid component API. You can also use LyteNyte Grid
in a headless fashion by providing children. To learn more about headless usage, see the
Headless Component Parts guide .
LyteNyte Grid reads data from a row data source. The most common option is a
client-side data source. Use the client-side data source when all row data is available in the browser.
Next you will:
Import the row data and the data source hook.
Create a row data source from the imported data and pass it to the grid.
import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { requestData , type RequestData } from " ./data.js " ;
const columns : Grid . Column < GridSpec > [] = [
{ id : " Date " , name : " Date " , width : 200 , type : " datetime " },
{ id : " Status " , name : " Status " , width : 100 },
{ id : " Method " , name : " Method " , width : 100 },
{ id : " timing-phase " , name : " Timing Phase " },
{ id : " Pathname " , name : " Pathname " },
{ id : " Latency " , name : " Latency " , width : 120 , type : " number " },
{ id : " region " , name : " Region " },
export default function GettingStarted () {
const ds = useClientDataSource < GridSpec > ( {
< div className = " ln-grid " style = {{ height: 400 }}>
< Grid columns = { columns } rowSource = { ds } />
The annotated code marks the important changes.
Import the useClientDataSource hook from the LyteNyte package. Use the client-side data source
hook when the browser already has all row data.
Import requestData from ./data.js. This guide assumes you downloaded the data for the demo
and created a data.js (or data.ts) file. You can download or copy the data file from the
LyteNyte Grid GitHub repository here .
Create a local data.js file in the same folder as the file that renders the grid.
Create a GridSpec type. LyteNyte Grid uses TypeScript for code completion and type checking.
The grid cannot know the row data type ahead of time, so you define a specification interface
and pass it as the type parameter to the relevant grid types.
The specification interface can do more than define the row data type, as will be covered in
a later section of this guide. For best practices on using TypeScript
with LyteNyte Grid, see the TypeScript guide .
Call the useClientDataSource hook and pass in requestData. Then set the grid’s rowSource
property to the ds value returned from useClientDataSource.
If you followed along to this point you will have a working grid, but a very plain looking view. The demo
below shows what the result looks like. The example looks plain, and not every column displays a value.
LyteNyte Grid’s default cell renderer only displays simple values, such as strings or numbers. For complex
values, you must provide a custom cell renderer, as will be demonstrated next.
Basic Grid import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { requestData , type RequestData } from " ./data.js " ;
export interface GridSpec {
const columns : Grid . Column < GridSpec > [] = [
{ id : " Date " , name : " Date " , width : 200 , type : " datetime " },
{ id : " Status " , name : " Status " , width : 100 },
{ id : " Method " , name : " Method " , width : 100 },
{ id : " timing-phase " , name : " Timing Phase " },
{ id : " Pathname " , name : " Pathname " },
{ id : " Latency " , name : " Latency " , width : 120 , type : " number " },
{ id : " region " , name : " Region " },
export default function GettingStartedDemo () {
const ds = useClientDataSource < GridSpec > ( {
< div className = " ln-grid " style = {{ height: 400 }}>
< Grid columns = { columns } rowSource = { ds } />
A custom cell renderer is a normal React component. LyteNyte Grid passes the cell renderer
cell-specific properties, defined by the CellRendererParams
type.
You set a cell renderer on the column definitions you pass to the grid. Start by defining a cell renderer
for the Timing Phase column. The code block below collapses the implementation, because a cell renderer can contain
any React content you want.
Focus on the function definition. You define a cell renderer by creating a React component
that accepts CellRendererParams. This example also uses the GridSpec type, defined earlier,
to improve type checking.
import type { CellRendererParams } from " @1771technologies/lytenyte-pro/types " ;
const colors = [ " var(--transfer) " , " var(--dns) " , " var(--connection) " , " var(--ttfb) " , " var(--tls) " ] ;
export function TimingPhaseCell ({ api , row } : CellRendererParams < GridSpec >) {
// Guard against rows that are not leaves or rows that have no data.
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
row . data [ " timing-phase.connection " ] +
row . data [ " timing-phase.dns " ] +
row . data [ " timing-phase.tls " ] +
row . data [ " timing-phase.transfer " ] +
row . data [ " timing-phase.ttfb " ] ;
const connectionPer = ( row . data [ " timing-phase.connection " ] / total ) * 100 ;
const dnsPer = ( row . data [ " timing-phase.dns " ] / total ) * 100 ;
const tlPer = ( row . data [ " timing-phase.tls " ] / total ) * 100 ;
const transferPer = ( row . data [ " timing-phase.transfer " ] / total ) * 100 ;
const ttfbPer = ( row . data [ " timing-phase.ttfb " ] / total ) * 100 ;
const values = [ connectionPer , dnsPer , tlPer , transferPer , ttfbPer ] ;
< div className = " flex h-full w-full items-center " >
< div className = " flex h-4 w-full items-center gap-px overflow-hidden " >
style = {{ width: `${ v } % ` , background: colors [ i ] }}
className = { clsx ( " h-full rounded-sm " )}
Now that you know how to define cell renderers, set the cellRenderer property on the
columns. The updated column code is shown below.
import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import type { RequestData } from " ./data.js " ;
import { requestData } from " ./data.js " ;
export interface GridSpec {
} from " ./components.js " ;
const columns : Grid . Column < GridSpec > [] = [
{ id : " Date " , name : " Date " , width : 200 , type : " datetime " , cellRenderer : DateCell },
{ id : " Status " , name : " Status " , width : 100 , cellRenderer : StatusCell },
{ id : " Method " , name : " Method " , width : 100 , cellRenderer : MethodCell },
{ id : " timing-phase " , name : " Timing Phase " , cellRenderer : TimingPhaseCell },
{ id : " Pathname " , name : " Pathname " , cellRenderer : PathnameCell },
{ id : " Latency " , name : " Latency " , width : 120 , type : " number " , cellRenderer : LatencyCell },
{ id : " region " , name : " Region " , cellRenderer : RegionCell },
export default function GettingStarted () {
const ds = useClientDataSource < GridSpec > ( {
< div className = " ln-grid " style = {{ height: 400 }}>
< Grid columns = { columns } rowSource = { ds } />
We use Tailwind CSS to style components. LyteNyte Grid has no opinion on which styling
framework you use. If you follow this guide line by line and want the cell renderers to
render correctly, set up Tailwind by following the Tailwind installation guide and
apply our Tailwind theme configuration .
The cell renderers also use custom CSS properties for colors. The CSS below defines those properties:
For general theming and best practices, see the Grid Theming guide .
The cell renderers live in the components.js file. You can create this file yourself or copy our implementation
from GitHub .
A full working example is shown below.
Cell Renderers import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import type { RequestData } from " ./data.js " ;
import { requestData } from " ./data.js " ;
} from " ./components.js " ;
export interface GridSpec {
const columns : Grid . Column < GridSpec > [] = [
{ id : " Date " , name : " Date " , width : 200 , type : " datetime " , cellRenderer : DateCell },
{ id : " Status " , name : " Status " , width : 100 , cellRenderer : StatusCell },
{ id : " Method " , name : " Method " , width : 100 , cellRenderer : MethodCell },
{ id : " timing-phase " , name : " Timing Phase " , cellRenderer : TimingPhaseCell },
{ id : " Pathname " , name : " Pathname " , cellRenderer : PathnameCell },
{ id : " Latency " , name : " Latency " , width : 120 , cellRenderer : LatencyCell },
{ id : " region " , name : " Region " , cellRenderer : RegionCell },
export default function GettingStartedDemo () {
const ds = useClientDataSource < GridSpec > ( {
< div className = " ln-grid " style = {{ height: 400 }}>
< Grid columns = { columns } rowSource = { ds } />
import type { GridSpec } from " ./demo " ;
import { useMemo } from " react " ;
import { format } from " date-fns " ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
export function DateCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const niceDate = useMemo ( () => {
if ( typeof field !== " string " ) return null ;
return format ( field , " MMM dd, yyyy HH:mm:ss " ) ;
// Guard against bad values and render nothing
if ( ! niceDate ) return null ;
return < div className = " text-ln-text flex h-full w-full items-center tabular-nums " >{ niceDate }</ div >;
export function StatusCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const status = api . columnField ( column , row ) ;
// Guard against bad values
if ( typeof status !== " number " ) return null ;
< div className = { clsx ( " flex h-full w-full items-center text-xs font-bold " )}>
status < 400 && " text-ln-primary-50 bg-[#126CFF1F] " ,
status >= 400 && status < 500 && " bg-[#FF991D1C] text-[#EEA760] " ,
status >= 500 && " bg-[#e63d3d2d] text-[#e63d3d] " ,
export function MethodCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const method = api . columnField ( column , row ) ;
// Guard against bad values
if ( typeof method !== " string " ) return null ;
< div className = { clsx ( " flex h-full w-full items-center text-xs font-bold " )}>
method === " GET " && " text-ln-primary-50 bg-[#126CFF1F] " ,
( method === " PATCH " || method === " PUT " || method === " POST " ) && " bg-[#FF991D1C] text-[#EEA760] " ,
method === " DELETE " && " bg-[#e63d3d2d] text-[#e63d3d] " ,
export function PathnameCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const path = api . columnField ( column , row ) ;
if ( typeof path !== " string " ) return null ;
< div className = " text-ln-text-dark flex h-full w-full items-center text-sm " >
< div className = " text-ln-primary-50 w-full overflow-hidden text-ellipsis text-nowrap " >{ path }</ div >
const numberFormatter = new Intl . NumberFormat ( " en-Us " , {
maximumFractionDigits : 0 ,
minimumFractionDigits : 0 ,
export function LatencyCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const ms = api . columnField ( column , row ) ;
if ( typeof ms !== " number " ) return null ;
< div className = " text-ln-text-dark flex h-full w-full items-center text-sm tabular-nums " >
< span className = " text-ln-gray-100 " >{ numberFormatter . format ( ms )}</ span >
< span className = " text-ln-text-light text-xs " >ms</ span >
export function RegionCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
// Only render for leaf rows and we have some data
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const shortName = row . data [ " region.shortname " ] ;
const longName = row . data [ " region.fullname " ] ;
< div className = " flex h-full w-full items-center " >
< div className = " flex items-baseline gap-2 text-sm " >
< div className = " text-ln-gray-100 " >{ shortName }</ div >
< div className = " text-ln-text-light leading-4 " >{ longName }</ div >
const colors = [ " var(--transfer) " , " var(--dns) " , " var(--connection) " , " var(--ttfb) " , " var(--tls) " ] ;
export function TimingPhaseCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
// Guard against rows that are not leafs or rows that have no data.
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
row . data [ " timing-phase.connection " ] +
row . data [ " timing-phase.dns " ] +
row . data [ " timing-phase.tls " ] +
row . data [ " timing-phase.transfer " ] +
row . data [ " timing-phase.ttfb " ] ;
const connectionPer = ( row . data [ " timing-phase.connection " ] / total ) * 100 ;
const dnsPer = ( row . data [ " timing-phase.dns " ] / total ) * 100 ;
const tlPer = ( row . data [ " timing-phase.tls " ] / total ) * 100 ;
const transferPer = ( row . data [ " timing-phase.transfer " ] / total ) * 100 ;
const ttfbPer = ( row . data [ " timing-phase.ttfb " ] / total ) * 100 ;
const values = [ connectionPer , dnsPer , tlPer , transferPer , ttfbPer ] ;
< div className = " flex h-full w-full items-center " >
< div className = " flex h-4 w-full items-center gap-px overflow-hidden " >
style = {{ width: `${ v } % ` , background: colors [ i ] }}
className = { clsx ( " h-full rounded-sm " )}
The grid now looks much better, but you can still add more features. In the sections that follow,
you will:
Add the ability to sort columns. You will do this by extending the column definition with a custom sort property.
Make every row a master-detail row that expands to reveal more information about the row data.
All the features in these sections work in both the Core and PRO editions of LyteNyte Grid, so you can follow along
regardless of which edition you use.
LyteNyte Grid can sort rows by a specific column. To enable sorting, you will:
Extend the column definition with a sort property.
Store the columns in React state, so you can update them through the LyteNyte Grid API.
Provide a custom header renderer that sorts by a column when the user clicks the header.
Start by extending the column definition in the GridSpec interface, as shown below:
export interface GridSpec {
column : { sort ?: " asc " | " desc " | null };
This change only affects TypeScript. If you use JavaScript, you can safely ignore this section.
Next, store the columns in React state. The simplest way to do this is with useState, as shown in the code below:
export default function GettingStarted () {
const [ columns , setColumns ] = useState < Grid . Column < GridSpec > [] > ([
{ id : " Date " , name : " Date " , width : 200 , type : " datetime " , cellRenderer : DateCell },
{ id : " Status " , name : " Status " , width : 100 , cellRenderer : StatusCell },
{ id : " Method " , name : " Method " , width : 100 , cellRenderer : MethodCell },
{ id : " timing-phase " , name : " Timing Phase " , cellRenderer : TimingPhaseCell },
{ id : " Pathname " , name : " Pathname " , cellRenderer : PathnameCell },
{ id : " Latency " , name : " Latency " , width : 120 , type : " number " , cellRenderer : LatencyCell },
{ id : " region " , name : " Region " , cellRenderer : RegionCell },
const ds = useClientDataSource < GridSpec > ( {
< div className = " demo ln-grid " style = {{ height: 400 }}>
onColumnsChange = { setColumns }
This code passes the state-backed columns to the grid, additionally it passes the setColumns state setter
to the onColumnsChange prop. This lets the grid update column state when the user interacts with
the grid.
With the column definitions in place, create a header renderer that sorts a column when the user clicks it. You can
set a header renderer on a column with the headerRenderer property. Since all columns use the same header renderer,
set headerRenderer on the base column through the grid’s columnBase property.
const base : Grid . Props < GridSpec > [ " columnBase " ] = {
export default function GettingStarted () {
const [ columns , setColumns ] = useState < Grid . Column < GridSpec > [] > ([
{ id : " Date " , name : " Date " , width : 200 , type : " datetime " , cellRenderer : DateCell },
{ id : " Status " , name : " Status " , width : 100 , cellRenderer : StatusCell },
{ id : " Method " , name : " Method " , width : 100 , cellRenderer : MethodCell },
{ id : " timing-phase " , name : " Timing Phase " , cellRenderer : TimingPhaseCell },
{ id : " Pathname " , name : " Pathname " , cellRenderer : PathnameCell },
{ id : " Latency " , name : " Latency " , width : 120 , type : " number " , cellRenderer : LatencyCell },
{ id : " region " , name : " Region " , cellRenderer : RegionCell },
const ds = useClientDataSource < GridSpec > ( {
< div className = " demo ln-grid " style = {{ height: 400 }}>
onColumnsChange = { setColumns }
Next, define the header renderer and use it to update column state. The code below uses an onClick handler to build
a column update by cycling through sort states for the clicked column. The handler then calls api.columnUpdate, which
produces an updated set of columns. When api.columnUpdate runs, the grid calls onColumnsChange with the new columns.
export function Header ({ api , column } : HeaderParams < GridSpec >) {
const columns = api . props (). columns ;
const updates : Record < string , Partial < Grid . Column < GridSpec >>> = {};
const columnsWithSort = columns . filter (( x ) => x . sort );
columnsWithSort . forEach (( x ) => {
updates [ x . id ] = { sort: null };
if ( column . sort === " asc " ) {
updates [ column . id ] = { sort: null };
} else if ( column . sort === " desc " ) {
updates [ column . id ] = { sort: " asc " };
updates [ column . id ] = { sort: " desc " };
api . columnUpdate ( updates );
< div className = " sort-button flex w-full items-center justify-between rounded px-1 py-1 transition-colors " >
{ column . name ?? column . id }
{ column . sort === " asc " && < ArrowUpIcon className = " text-ln-text-dark size-4 " />}
{ column . sort === " desc " && < ArrowDownIcon className = " text-ln-text-dark size-4 " />}
Finally, pass a sort model to the client row data source. Create the sort model by deriving the value
from the column state. The code below derives a sort model from the columns which have a sort value
set on their specification.
export default function GettingStarted () {
const [ columns , setColumns ] = useState < Grid . Column < GridSpec > [] > ([
{ id : " Date " , name : " Date " , width : 200 , type : " datetime " , cellRenderer : DateCell },
{ id : " Status " , name : " Status " , width : 100 , cellRenderer : StatusCell },
{ id : " Method " , name : " Method " , width : 100 , cellRenderer : MethodCell },
{ id : " timing-phase " , name : " Timing Phase " , cellRenderer : TimingPhaseCell },
{ id : " Pathname " , name : " Pathname " , cellRenderer : PathnameCell },
{ id : " Latency " , name : " Latency " , width : 120 , type : " number " , cellRenderer : LatencyCell },
{ id : " region " , name : " Region " , cellRenderer : RegionCell },
const sort = useMemo < UseClientDataSourceParams < GridSpec > [ " sort " ] > ( () => {
const colWithSort = columns . find ( ( x ) => x . sort ) ;
if ( ! colWithSort ) return null ;
if ( sortComparators [ colWithSort . id ])
return [ { dim : sortComparators [ colWithSort . id ] , descending : colWithSort . sort === " desc " } ] ;
return [ { dim : colWithSort , descending : colWithSort . sort === " desc " } ] ;
const ds = useClientDataSource < GridSpec > ( {
< div className = " demo ln-grid " style = {{ height: 400 }}>
onColumnsChange = { setColumns }
rowDetailRenderer = { RowDetailRenderer }
A full working example with column sorting is shown below.
Sorting Demo import " @1771technologies/lytenyte-pro/light-dark.css " ;
type UseClientDataSourceParams ,
} from " @1771technologies/lytenyte-pro " ;
import type { RequestData } from " ./data.js " ;
import { requestData } from " ./data.js " ;
} from " ./components.js " ;
import { useMemo , useState } from " react " ;
import { sortComparators } from " ./comparators.js " ;
export interface GridSpec {
column : { sort ?: " asc " | " desc " | null };
const base : Grid . Props < GridSpec > [ " columnBase " ] = {
export default function GettingStartedDemo () {
const [ columns , setColumns ] = useState < Grid . Column < GridSpec > [] > ([
{ id : " Date " , name : " Date " , width : 200 , type : " datetime " , cellRenderer : DateCell },
{ id : " Status " , name : " Status " , width : 100 , cellRenderer : StatusCell },
{ id : " Method " , name : " Method " , width : 100 , cellRenderer : MethodCell },
{ id : " timing-phase " , name : " Timing Phase " , cellRenderer : TimingPhaseCell },
{ id : " Pathname " , name : " Pathname " , cellRenderer : PathnameCell },
{ id : " Latency " , name : " Latency " , width : 120 , type : " number " , cellRenderer : LatencyCell },
{ id : " region " , name : " Region " , cellRenderer : RegionCell },
const sort = useMemo < UseClientDataSourceParams < GridSpec > [ " sort " ] > ( () => {
const colWithSort = columns . find ( ( x ) => x . sort ) ;
if ( ! colWithSort ) return null ;
if ( sortComparators [ colWithSort . id ])
return [ { dim : sortComparators [ colWithSort . id ] , descending : colWithSort . sort === " desc " } ] ;
return [ { dim : colWithSort , descending : colWithSort . sort === " desc " } ] ;
const ds = useClientDataSource < GridSpec > ( {
< div className = " demo ln-grid " style = {{ height: 400 }}>
< Grid columns = { columns } onColumnsChange = { setColumns } columnBase = { base } rowSource = { ds } />
import type { GridSpec } from " ./demo " ;
import { useMemo } from " react " ;
import { format } from " date-fns " ;
import { ArrowDownIcon , ArrowUpIcon , ChevronDownIcon , ChevronRightIcon } from " @radix-ui/react-icons " ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
export function Header ({ api , column } : Grid . T . HeaderParams < GridSpec >) {
className = " text-ln-gray-60 group relative flex h-full w-full cursor-pointer items-center px-1 text-sm transition-colors "
const columns = api . props (). columns ;
const updates : Record < string , Partial < Grid . Column < GridSpec >>> = {};
const columnWithSort = columns . filter (( x ) => x . sort );
columnWithSort . forEach (( x ) => {
updates [ x . id ] = { sort: null };
if ( column . sort === " asc " ) {
updates [ column . id ] = { sort: null };
} else if ( column . sort === " desc " ) {
updates [ column . id ] = { sort: " asc " };
updates [ column . id ] = { sort: " desc " };
api . columnUpdate ( updates );
< div className = " sort-button flex w-full items-center justify-between rounded px-1 py-1 transition-colors " >
{ column . name ?? column . id }
{ column . sort === " asc " && < ArrowUpIcon className = " text-ln-text-dark size-4 " />}
{ column . sort === " desc " && < ArrowDownIcon className = " text-ln-text-dark size-4 " />}
export function MarkerCell ({ detailExpanded , row , api } : Grid . T . CellRendererParams < GridSpec >) {
className = " text-ln-text-dark flex h-full w-[calc(100%-1px)] items-center justify-center pl-2 "
onClick = {() => api . rowDetailToggle ( row )}
< ChevronDownIcon width = { 20 } height = { 20 } />
< ChevronRightIcon width = { 20 } height = { 20 } />
export function DateCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const niceDate = useMemo ( () => {
if ( typeof field !== " string " ) return null ;
return format ( field , " MMM dd, yyyy HH:mm:ss " ) ;
// Guard against bad values and render nothing
if ( ! niceDate ) return null ;
return < div className = " text-ln-text flex h-full w-full items-center tabular-nums " >{ niceDate }</ div >;
export function StatusCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const status = api . columnField ( column , row ) ;
// Guard against bad values
if ( typeof status !== " number " ) return null ;
< div className = { clsx ( " flex h-full w-full items-center text-xs font-bold " )}>
status < 400 && " text-ln-primary-50 bg-[#126CFF1F] " ,
status >= 400 && status < 500 && " bg-[#FF991D1C] text-[#EEA760] " ,
status >= 500 && " bg-[#e63d3d2d] text-[#e63d3d] " ,
export function MethodCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const method = api . columnField ( column , row ) ;
// Guard against bad values
if ( typeof method !== " string " ) return null ;
< div className = { clsx ( " flex h-full w-full items-center text-xs font-bold " )}>
method === " GET " && " text-ln-primary-50 bg-[#126CFF1F] " ,
( method === " PATCH " || method === " PUT " || method === " POST " ) && " bg-[#FF991D1C] text-[#EEA760] " ,
method === " DELETE " && " bg-[#e63d3d2d] text-[#e63d3d] " ,
export function PathnameCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const path = api . columnField ( column , row ) ;
if ( typeof path !== " string " ) return null ;
< div className = " text-ln-text-dark flex h-full w-full items-center text-sm " >
< div className = " text-ln-primary-50 w-full overflow-hidden text-ellipsis text-nowrap " >{ path }</ div >
const numberFormatter = new Intl . NumberFormat ( " en-Us " , {
maximumFractionDigits : 0 ,
minimumFractionDigits : 0 ,
export function LatencyCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const ms = api . columnField ( column , row ) ;
if ( typeof ms !== " number " ) return null ;
< div className = " text-ln-text-dark flex h-full w-full items-center text-sm tabular-nums " >
< span className = " text-ln-gray-100 " >{ numberFormatter . format ( ms )}</ span >
< span className = " text-ln-text-light text-xs " >ms</ span >
export function RegionCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
// Only render for leaf rows and we have some data
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const shortName = row . data [ " region.shortname " ] ;
const longName = row . data [ " region.fullname " ] ;
< div className = " flex h-full w-full items-center " >
< div className = " flex items-baseline gap-2 text-sm " >
< div className = " text-ln-gray-100 " >{ shortName }</ div >
< div className = " text-ln-text-light leading-4 " >{ longName }</ div >
const colors = [ " var(--transfer) " , " var(--dns) " , " var(--connection) " , " var(--ttfb) " , " var(--tls) " ] ;
export function TimingPhaseCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
// Guard against rows that are not leafs or rows that have no data.
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
row . data [ " timing-phase.connection " ] +
row . data [ " timing-phase.dns " ] +
row . data [ " timing-phase.tls " ] +
row . data [ " timing-phase.transfer " ] +
row . data [ " timing-phase.ttfb " ] ;
const connectionPer = ( row . data [ " timing-phase.connection " ] / total ) * 100 ;
const dnsPer = ( row . data [ " timing-phase.dns " ] / total ) * 100 ;
const tlPer = ( row . data [ " timing-phase.tls " ] / total ) * 100 ;
const transferPer = ( row . data [ " timing-phase.transfer " ] / total ) * 100 ;
const ttfbPer = ( row . data [ " timing-phase.ttfb " ] / total ) * 100 ;
const values = [ connectionPer , dnsPer , tlPer , transferPer , ttfbPer ] ;
< div className = " flex h-full w-full items-center " >
< div className = " flex h-4 w-full items-center gap-px overflow-hidden " >
style = {{ width: `${ v } % ` , background: colors [ i ] }}
className = { clsx ( " h-full rounded-sm " )}
import type { GridSpec } from " ./demo.js " ;
import type { RequestData } from " ./data.js " ;
import { compareAsc } from " date-fns " ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
export const sortComparators : Record < string , Grid . T . SortFn < GridSpec [ " data " ] >> = {
region : ( left , right ) => {
if ( left . kind !== " leaf " && right . kind !== " leaf " ) return 0 ;
if ( left . kind === " leaf " && right . kind !== " leaf " ) return - 1 ;
if ( left . kind !== " leaf " && right . kind === " leaf " ) return 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 !== " leaf " && right . kind !== " leaf " ) return 0 ;
if ( left . kind === " leaf " && right . kind !== " leaf " ) return - 1 ;
if ( left . kind !== " leaf " && right . kind === " leaf " ) return 1 ;
const leftData = left . data as RequestData ;
const rightData = right . data as RequestData ;
return leftData . Latency - rightData . Latency ;
if ( left . kind !== " leaf " && right . kind !== " leaf " ) return 0 ;
if ( left . kind === " leaf " && right . kind !== " leaf " ) return - 1 ;
if ( left . kind !== " leaf " && right . kind === " leaf " ) return 1 ;
const leftData = left . data as RequestData ;
const rightData = right . data as RequestData ;
return compareAsc ( leftData . Date , rightData . Date ) ;
LyteNyte Grid supports row detail information, also known as master detail.
This section covers the basic setup and functionality. For more information, see the
Row Master Detail guide .
To enable row master detail, you will:
Define a detail renderer.
Enable LyteNyte Grid’s marker column and provide a cell renderer that
toggles the row detail expansion state.
A row detail renderer is a React component that LyteNyte Grid uses to render the detail area for an expanded
row. Set the renderer on the grid’s rowDetailRenderer property, as shown below:
onColumnsChange = { setColumns }
rowDetailRenderer = { RowDetailRenderer }
The renderer is a normal React component that receives row props from LyteNyte Grid. The definition of a detail
renderer is shown below. The content is collapsed because the important line is the function definition. A row
detail renderer can return any React content. The key requirements are that the function returns a valid
ReactNode and accepts RowParams as a prop.
export function RowDetailRenderer ({ row , api } : RowParams < GridSpec >) {
// Guard against empty data.
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
row . data [ " timing-phase.connection " ] +
row . data [ " timing-phase.dns " ] +
row . data [ " timing-phase.tls " ] +
row . data [ " timing-phase.transfer " ] +
row . data [ " timing-phase.ttfb " ] ;
const connectionPer = ( row . data [ " timing-phase.connection " ] / total ) * 100 ;
const dnsPer = ( row . data [ " timing-phase.dns " ] / total ) * 100 ;
const tlPer = ( row . data [ " timing-phase.tls " ] / total ) * 100 ;
const transferPer = ( row . data [ " timing-phase.transfer " ] / total ) * 100 ;
const ttfbPer = ( row . data [ " timing-phase.ttfb " ] / total ) * 100 ;
< div className = " pt-1.75 flex h-full flex-col px-4 pb-5 text-sm " >
< h3 className = " text-ln-text-xlight mt-0 text-xs font-medium " >Timing Phases</ h3 >
< div className = " flex flex-1 gap-2 pt-1.5 " >
< div className = " bg-ln-gray-00 border-ln-gray-20 h-full flex-1 rounded-[10px] border " >
< div className = " grid-cols[auto_auto_1fr] grid grid-rows-5 gap-1 gap-x-4 p-4 md:grid-cols-[auto_auto_200px_auto] " >
msPercentage = { transferPer }
msValue = { row . data [ " timing-phase.transfer " ]}
msValue = { row . data [ " timing-phase.dns " ]}
msPercentage = { connectionPer }
msValue = { row . data [ " timing-phase.connection " ]}
msValue = { row . data [ " timing-phase.ttfb " ]}
msValue = { row . data [ " timing-phase.tls " ]}
< div className = " col-start-3 row-span-full flex h-full flex-1 items-center justify-center " >
< TimingPhasePieChart row = { row . data } />
After you define the row detail renderer, you need a way to toggle the visibility of each detail row.
LyteNyte Grid’s marker column provides the perfect place to add this functionality.
The marker column is a special column that LyteNyte Grid creates and manages. LyteNyte always pins the marker
column to the start of the grid, and the marker column does not appear in the columns array you
pass to the grid. Use the marker column for auxiliary row actions such as row selection or row detail expansion.
To enable the marker column, set the grid’s columnMarker property to a marker column definition, as shown below:
const marker : Grid . Props < GridSpec > [ " columnMarker " ] = {
cellRenderer : MarkerCell ,
export default function GettingStarted () {
const [ columns , setColumns ] = useState < Grid . Column < GridSpec > [] > ([
{ id : " Date " , name : " Date " , width : 200 , type : " datetime " , cellRenderer : DateCell },
{ id : " Status " , name : " Status " , width : 100 , cellRenderer : StatusCell },
{ id : " Method " , name : " Method " , width : 100 , cellRenderer : MethodCell },
{ id : " timing-phase " , name : " Timing Phase " , cellRenderer : TimingPhaseCell },
{ id : " Pathname " , name : " Pathname " , cellRenderer : PathnameCell },
{ id : " Latency " , name : " Latency " , width : 120 , type : " number " , cellRenderer : LatencyCell },
{ id : " region " , name : " Region " , cellRenderer : RegionCell },
const sort = useMemo < UseClientDataSourceParams < GridSpec > [ " sort " ] > ( () => {
const colWithSort = columns . find ( ( x ) => x . sort ) ;
if ( ! colWithSort ) return null ;
if ( sortComparators [ colWithSort . id ])
return [ { dim : sortComparators [ colWithSort . id ] , descending : colWithSort . sort === " desc " } ] ;
return [ { dim : colWithSort , descending : colWithSort . sort === " desc " } ] ;
const ds = useClientDataSource < GridSpec > ( {
< div className = " demo ln-grid " style = {{ height: 400 }}>
onColumnsChange = { setColumns }
rowDetailRenderer = { RowDetailRenderer }
The marker column uses the MarkerCell component as its cell renderer. The MarkerCell definition is shown
below. Focus on the api.rowDetailToggle method, which toggles the row detail expansion state for a given row.
Use this method to show and hide row detail areas.
export function MarkerCell ({ detailExpanded , row , api } : CellRendererParams < GridSpec >) {
< button onClick = {() => api . rowDetailToggle ( row )}>
< ChevronDownIcon width = { 20 } height = { 20 } />
< ChevronRightIcon width = { 20 } height = { 20 } />
Putting everything together, the full working example is shown below:
Complete Getting Started Demo import " @1771technologies/lytenyte-pro/light-dark.css " ;
type UseClientDataSourceParams ,
} from " @1771technologies/lytenyte-pro " ;
import type { RequestData } from " ./data.js " ;
import { requestData } from " ./data.js " ;
} from " ./components.js " ;
import { useMemo , useState } from " react " ;
import { sortComparators } from " ./comparators.js " ;
export interface GridSpec {
column : { sort ?: " asc " | " desc " | null };
const base : Grid . Props < GridSpec > [ " columnBase " ] = {
const marker : Grid . Props < GridSpec > [ " columnMarker " ] = {
headerRenderer : () => < div className = " sr-only " >Toggle row detail expansion</ div >,
cellRenderer : MarkerCell ,
export default function GettingStartedDemo () {
const [ columns , setColumns ] = useState < Grid . Column < GridSpec > [] > ([
{ id : " Date " , name : " Date " , width : 200 , type : " datetime " , cellRenderer : DateCell },
{ id : " Status " , name : " Status " , width : 100 , cellRenderer : StatusCell },
{ id : " Method " , name : " Method " , width : 100 , cellRenderer : MethodCell },
{ id : " timing-phase " , name : " Timing Phase " , cellRenderer : TimingPhaseCell },
{ id : " Pathname " , name : " Pathname " , cellRenderer : PathnameCell },
{ id : " Latency " , name : " Latency " , width : 120 , type : " number " , cellRenderer : LatencyCell },
{ id : " region " , name : " Region " , cellRenderer : RegionCell },
const sort = useMemo < UseClientDataSourceParams < GridSpec > [ " sort " ] > ( () => {
const colWithSort = columns . find ( ( x ) => x . sort ) ;
if ( ! colWithSort ) return null ;
if ( sortComparators [ colWithSort . id ])
return [ { dim : sortComparators [ colWithSort . id ] , descending : colWithSort . sort === " desc " } ] ;
return [ { dim : colWithSort , descending : colWithSort . sort === " desc " } ] ;
const ds = useClientDataSource < GridSpec > ( {
< div className = " demo ln-grid " style = {{ height: 400 }}>
onColumnsChange = { setColumns }
rowDetailRenderer = { RowDetailRenderer }
import type { GridSpec } from " ./demo " ;
import { useMemo } from " react " ;
import { format } from " date-fns " ;
import type { RequestData } from " ./data " ;
import { PieChart } from " react-minimal-pie-chart " ;
import { ArrowDownIcon , ArrowUpIcon , ChevronDownIcon , ChevronRightIcon } from " @radix-ui/react-icons " ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
export function Header ({ api , column } : Grid . T . HeaderParams < GridSpec >) {
className = " text-ln-gray-60 group relative flex h-full w-full cursor-pointer items-center px-1 text-sm transition-colors "
const columns = api . props (). columns ;
const updates : Record < string , Partial < Grid . Column < GridSpec >>> = {};
const columnWithSort = columns . filter (( x ) => x . sort );
columnWithSort . forEach (( x ) => {
updates [ x . id ] = { sort: null };
if ( column . sort === " asc " ) {
updates [ column . id ] = { sort: null };
} else if ( column . sort === " desc " ) {
updates [ column . id ] = { sort: " asc " };
updates [ column . id ] = { sort: " desc " };
api . columnUpdate ( updates );
< div className = " sort-button flex w-full items-center justify-between rounded px-1 py-1 transition-colors " >
{ column . name ?? column . id }
{ column . sort === " asc " && < ArrowUpIcon className = " text-ln-text-dark size-4 " />}
{ column . sort === " desc " && < ArrowDownIcon className = " text-ln-text-dark size-4 " />}
export function MarkerCell ({ detailExpanded , row , api } : Grid . T . CellRendererParams < GridSpec >) {
className = " text-ln-text flex h-full w-[calc(100%-1px)] cursor-pointer items-center justify-center "
onClick = {() => api . rowDetailToggle ( row )}
< ChevronDownIcon width = { 20 } height = { 20 } />
< ChevronRightIcon width = { 20 } height = { 20 } />
export function DateCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const niceDate = useMemo ( () => {
if ( typeof field !== " string " ) return null ;
return format ( field , " MMM dd, yyyy HH:mm:ss " ) ;
// Guard against bad values and render nothing
if ( ! niceDate ) return null ;
return < div className = " text-ln-text flex h-full w-full items-center tabular-nums " >{ niceDate }</ div >;
export function StatusCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const status = api . columnField ( column , row ) ;
// Guard against bad values
if ( typeof status !== " number " ) return null ;
< div className = { clsx ( " flex h-full w-full items-center text-xs font-bold " )}>
status < 400 && " text-ln-primary-50 bg-[#126CFF1F] " ,
status >= 400 && status < 500 && " bg-[#FF991D1C] text-[#EEA760] " ,
status >= 500 && " bg-[#e63d3d2d] text-[#e63d3d] " ,
export function MethodCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const method = api . columnField ( column , row ) ;
// Guard against bad values
if ( typeof method !== " string " ) return null ;
< div className = { clsx ( " flex h-full w-full items-center text-xs font-bold " )}>
method === " GET " && " text-ln-primary-50 bg-[#126CFF1F] " ,
( method === " PATCH " || method === " PUT " || method === " POST " ) && " bg-[#FF991D1C] text-[#EEA760] " ,
method === " DELETE " && " bg-[#e63d3d2d] text-[#e63d3d] " ,
export function PathnameCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const path = api . columnField ( column , row ) ;
if ( typeof path !== " string " ) return null ;
< div className = " text-ln-text-dark flex h-full w-full items-center text-sm " >
< div className = " text-ln-primary-50 w-full overflow-hidden text-ellipsis text-nowrap " >{ path }</ div >
const numberFormatter = new Intl . NumberFormat ( " en-Us " , {
maximumFractionDigits : 0 ,
minimumFractionDigits : 0 ,
export function LatencyCell ({ column , row , api } : Grid . T . CellRendererParams < GridSpec >) {
const ms = api . columnField ( column , row ) ;
if ( typeof ms !== " number " ) return null ;
< div className = " text-ln-text-dark flex h-full w-full items-center text-sm tabular-nums " >
< span className = " text-ln-gray-100 " >{ numberFormatter . format ( ms )}</ span >
< span className = " text-ln-text-light text-xs " >ms</ span >
export function RegionCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
// Only render for leaf rows and we have some data
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const shortName = row . data [ " region.shortname " ] ;
const longName = row . data [ " region.fullname " ] ;
< div className = " flex h-full w-full items-center " >
< div className = " flex items-baseline gap-2 text-sm " >
< div className = " text-ln-gray-100 " >{ shortName }</ div >
< div className = " text-ln-text-light leading-4 " >{ longName }</ div >
const colors = [ " var(--transfer) " , " var(--dns) " , " var(--connection) " , " var(--ttfb) " , " var(--tls) " ] ;
export function TimingPhaseCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
// Guard against rows that are not leafs or rows that have no data.
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
row . data [ " timing-phase.connection " ] +
row . data [ " timing-phase.dns " ] +
row . data [ " timing-phase.tls " ] +
row . data [ " timing-phase.transfer " ] +
row . data [ " timing-phase.ttfb " ] ;
const connectionPer = ( row . data [ " timing-phase.connection " ] / total ) * 100 ;
const dnsPer = ( row . data [ " timing-phase.dns " ] / total ) * 100 ;
const tlPer = ( row . data [ " timing-phase.tls " ] / total ) * 100 ;
const transferPer = ( row . data [ " timing-phase.transfer " ] / total ) * 100 ;
const ttfbPer = ( row . data [ " timing-phase.ttfb " ] / total ) * 100 ;
const values = [ connectionPer , dnsPer , tlPer , transferPer , ttfbPer ] ;
< div className = " flex h-full w-full items-center " >
< div className = " flex h-4 w-full items-center gap-px overflow-hidden " >
style = {{ width: `${ v } % ` , background: colors [ i ] }}
className = { clsx ( " h-full rounded-sm " )}
export function RowDetailRenderer ({ row , api } : Grid . T . RowParams < GridSpec >) {
// Guard against empty data.
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
row . data [ " timing-phase.connection " ] +
row . data [ " timing-phase.dns " ] +
row . data [ " timing-phase.tls " ] +
row . data [ " timing-phase.transfer " ] +
row . data [ " timing-phase.ttfb " ] ;
const connectionPer = ( row . data [ " timing-phase.connection " ] / total ) * 100 ;
const dnsPer = ( row . data [ " timing-phase.dns " ] / total ) * 100 ;
const tlPer = ( row . data [ " timing-phase.tls " ] / total ) * 100 ;
const transferPer = ( row . data [ " timing-phase.transfer " ] / total ) * 100 ;
const ttfbPer = ( row . data [ " timing-phase.ttfb " ] / total ) * 100 ;
< div className = " pt-1.75 flex h-full flex-col px-4 pb-5 text-sm " >
< h3 className = " text-ln-text-xlight mt-0 text-xs font-medium " >Timing Phases</ h3 >
< div className = " flex flex-1 gap-2 pt-1.5 " >
< div className = " bg-ln-gray-00 border-ln-gray-20 h-full flex-1 rounded-[10px] border " >
< div className = " grid-cols[auto_auto_1fr] grid grid-rows-5 gap-1 gap-x-4 p-4 md:grid-cols-[auto_auto_200px_auto] " >
msPercentage = { transferPer }
msValue = { row . data [ " timing-phase.transfer " ]}
msValue = { row . data [ " timing-phase.dns " ]}
msPercentage = { connectionPer }
msValue = { row . data [ " timing-phase.connection " ]}
msValue = { row . data [ " timing-phase.ttfb " ]}
msValue = { row . data [ " timing-phase.tls " ]}
< div className = " col-start-3 row-span-full flex h-full flex-1 items-center justify-center " >
< TimingPhasePieChart row = { row . data } />
interface TimePhaseRowProps {
readonly msValue : number ;
readonly msPercentage : number ;
function TimingPhaseRow ({ color , msValue , msPercentage , label } : TimePhaseRowProps ) {
< div className = " text-sm " >{ label }</ div >
< div className = " text-sm tabular-nums " >{ msPercentage . toFixed ( 2 )}%</ div >
< div className = " col-start-4 hidden items-center justify-end gap-1 text-sm md:flex " >
< span className = " text-ln-gray-100 " >{ numberFormatter . format ( msValue )}</ span >
< span className = " text-ln-text-xlight text-xs " >ms</ span >
function TimingPhasePieChart ({ row } : { row : RequestData }) {
const data = useMemo ( () => {
{ subject : " Transfer " , value : row [ " timing-phase.transfer " ] , color : colors [ 0 ] },
{ subject : " DNS " , value : row [ " timing-phase.dns " ] , color : colors [ 1 ] },
{ subject : " Connection " , value : row [ " timing-phase.connection " ] , color : colors [ 2 ] },
{ subject : " TTFB " , value : row [ " timing-phase.ttfb " ] , color : colors [ 3 ] },
{ subject : " TLS " , value : row [ " timing-phase.tls " ] , color : colors [ 4 ] },
< div style = {{ height: 100 }}>
< PieChart data = { data } startAngle = { 180 } lengthAngle = { 180 } center = {[ 50 , 75 ]} paddingAngle = { 1 } />
import type { GridSpec } from " ./demo.js " ;
import type { RequestData } from " ./data.js " ;
import { compareAsc } from " date-fns " ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
export const sortComparators : Record < string , Grid . T . SortFn < GridSpec [ " data " ] >> = {
region : ( left , right ) => {
if ( left . kind !== " leaf " && right . kind !== " leaf " ) return 0 ;
if ( left . kind === " leaf " && right . kind !== " leaf " ) return - 1 ;
if ( left . kind !== " leaf " && right . kind === " leaf " ) return 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 !== " leaf " && right . kind !== " leaf " ) return 0 ;
if ( left . kind === " leaf " && right . kind !== " leaf " ) return - 1 ;
if ( left . kind !== " leaf " && right . kind === " leaf " ) return 1 ;
const leftData = left . data as RequestData ;
const rightData = right . data as RequestData ;
return leftData . Latency - rightData . Latency ;
if ( left . kind !== " leaf " && right . kind !== " leaf " ) return 0 ;
if ( left . kind === " leaf " && right . kind !== " leaf " ) return - 1 ;
if ( left . kind !== " leaf " && right . kind === " leaf " ) return 1 ;
const leftData = left . data as RequestData ;
const rightData = right . data as RequestData ;
return compareAsc ( leftData . Date , rightData . Date ) ;
Columns Overview : Learn how column configuration influences the grid view. Grid Theming : Review the general theming guide to learn more about styling LyteNyte Grid. Grid Reactivity : Learn how LyteNyte Grid enables declarative reactivity.