Rows
Row Selection Copy Page Open additional page export options
Select individual or multiple rows using LyteNyte Grid's row selection system. Easily implement checkbox-based selection with support for bulk operations.
The rowSelectionMode property on LyteNyte Grid configures the row selection behavior the grid uses.
It accepts one of the following values:
"none": Disables all built-in row selection behavior and prevents user interactions
from selecting rows.
"single": Allows the user to select a single row. Selecting a new row clears any existing selection.
"multiple": Allows multiple rows to be selected, including additive selection behavior.
In addition to rowSelectionMode, the rowSelectionActivator property controls
which user action selects a row. It accepts one of the following values:
"none": Disables all row-based selection interactions. Developers must provide
a custom selection mechanism, such as selection checkboxes.
"single-click": Selects a row when the user clicks it.
"double-click": Selects a row when the user double-clicks it.
The demo below shows single-row selection configured to select a row with
a single click. Other configurations, covered in later sections, support
multiple selection modes or checkbox-based selection.
Enabling Row Selection import " @1771technologies/lytenyte-pro/light-dark.css " ;
import " @1771technologies/lytenyte-pro/pill-manager.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
PercentCellPositiveNegative ,
} from " ./components.jsx " ;
import type { DEXPerformanceData } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
import { data } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
export interface GridSpec {
readonly data : DEXPerformanceData ;
const columns : Grid . Column < GridSpec > [] = [
{ id : " symbol " , cellRenderer : SymbolCell , width : 250 , name : " Symbol " },
{ id : " network " , cellRenderer : NetworkCell , width : 220 , hide : true , name : " Network " },
{ id : " exchange " , cellRenderer : ExchangeCell , width : 220 , hide : true , name : " Exchange " },
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Change " , " 24h " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 1w " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 1m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 3m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 6m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " YTD " ) ,
{ id : " volatility " , cellRenderer : PercentCell , name : " Volatility " , type : " number " },
cellRenderer : PercentCell ,
headerRenderer : makePerfHeaderCell ( " Volatility " , " 1m " ) ,
const base : Grid . ColumnBase < GridSpec > = { width : 80 };
export default function RowDemo () {
const ds = useClientDataSource ( { data : data } ) ;
className = " ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight "
rowSelectionMode = " single "
rowSelectionActivator = " single-click "
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import { exchanges , networks , symbols } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import type { GridSpec } from " ./demo " ;
export function SymbolCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const ticker = row . data . symbolTicker ;
const symbol = row . data . symbol ;
const image = symbols [ row . data . symbolTicker ] ;
< div className = " grid grid-cols-[20px_auto_auto] items-center gap-1.5 " >
alt = { ` Logo for symbol ${ symbol }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px] " >
< div className = " w-full overflow-hidden text-ellipsis " >{ symbol . split ( " / " )[ 0 ]}</ div >
export function NetworkCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const name = row . data . network ;
const image = networks [ name ] ;
< div className = " grid grid-cols-[20px_1fr] items-center gap-1.5 " >
alt = { ` Logo for network ${ name }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " w-full overflow-hidden text-ellipsis " >{ name }</ div >
export function ExchangeCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const name = row . data . exchange ;
const image = exchanges [ name ] ;
< div className = " grid grid-cols-[20px_1fr] items-center gap-1.5 " >
alt = { ` Logo for exchange ${ name }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " w-full overflow-hidden text-ellipsis " >{ name }</ div >
export function PercentCellPositiveNegative ({ api , column , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return " - " ;
const value = ( field > 0 ? " + " : "" ) + ( field * 100 ) . toFixed ( 2 ) + " % " ;
" h-ful flex w-full items-center justify-end tabular-nums " ,
field < 0 ? " text-red-600 dark:text-red-300 " : " text-green-600 dark:text-green-300 " ,
export function PercentCell ({ api , column , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return " - " ;
const value = ( field > 0 ? " + " : "" ) + ( field * 100 ) . toFixed ( 2 ) + " % " ;
return < div className = " h-ful flex w-full items-center justify-end tabular-nums " >{ value }</ div >;
export const makePerfHeaderCell = ( name : string , subname : string ) => {
return ( _ : Grid . T . HeaderParams < GridSpec >) => {
< div className = " flex h-full w-full flex-col items-end justify-center tabular-nums " >
< div className = " text-ln-text-light font-mono uppercase " >{ subname }</ div >
Set rowSelectionMode to "multiple" to allow users to select more than one row.
Clicking a row toggles its selection state. Holding Shift while clicking
another row selects the range between the two rows.
The demo below demonstrates these interactions.
Multiple Row Selection import " @1771technologies/lytenyte-pro/light-dark.css " ;
import " @1771technologies/lytenyte-pro/pill-manager.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
PercentCellPositiveNegative ,
} from " ./components.jsx " ;
import type { DEXPerformanceData } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
import { data } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
export interface GridSpec {
readonly data : DEXPerformanceData ;
const columns : Grid . Column < GridSpec > [] = [
{ id : " symbol " , cellRenderer : SymbolCell , width : 250 , name : " Symbol " },
{ id : " network " , cellRenderer : NetworkCell , width : 220 , hide : true , name : " Network " },
{ id : " exchange " , cellRenderer : ExchangeCell , width : 220 , hide : true , name : " Exchange " },
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Change " , " 24h " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 1w " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 1m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 3m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 6m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " YTD " ) ,
{ id : " volatility " , cellRenderer : PercentCell , name : " Volatility " , type : " number " },
cellRenderer : PercentCell ,
headerRenderer : makePerfHeaderCell ( " Volatility " , " 1m " ) ,
const base : Grid . ColumnBase < GridSpec > = { width : 80 };
export default function RowDemo () {
const ds = useClientDataSource ( { data : data , rowsIsolatedSelection : true } ) ;
className = " ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight "
rowSelectionMode = " multiple "
rowSelectionActivator = " single-click "
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import { exchanges , networks , symbols } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import type { GridSpec } from " ./demo " ;
export function SymbolCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const ticker = row . data . symbolTicker ;
const symbol = row . data . symbol ;
const image = symbols [ row . data . symbolTicker ] ;
< div className = " grid grid-cols-[20px_auto_auto] items-center gap-1.5 " >
alt = { ` Logo for symbol ${ symbol }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px] " >
< div className = " w-full overflow-hidden text-ellipsis " >{ symbol . split ( " / " )[ 0 ]}</ div >
export function NetworkCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const name = row . data . network ;
const image = networks [ name ] ;
< div className = " grid grid-cols-[20px_1fr] items-center gap-1.5 " >
alt = { ` Logo for network ${ name }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " w-full overflow-hidden text-ellipsis " >{ name }</ div >
export function ExchangeCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const name = row . data . exchange ;
const image = exchanges [ name ] ;
< div className = " grid grid-cols-[20px_1fr] items-center gap-1.5 " >
alt = { ` Logo for exchange ${ name }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " w-full overflow-hidden text-ellipsis " >{ name }</ div >
export function PercentCellPositiveNegative ({ api , column , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return " - " ;
const value = ( field > 0 ? " + " : "" ) + ( field * 100 ) . toFixed ( 2 ) + " % " ;
" h-ful flex w-full items-center justify-end tabular-nums " ,
field < 0 ? " text-red-600 dark:text-red-300 " : " text-green-600 dark:text-green-300 " ,
export function PercentCell ({ api , column , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return " - " ;
const value = ( field > 0 ? " + " : "" ) + ( field * 100 ) . toFixed ( 2 ) + " % " ;
return < div className = " h-ful flex w-full items-center justify-end tabular-nums " >{ value }</ div >;
export const makePerfHeaderCell = ( name : string , subname : string ) => {
return ( _ : Grid . T . HeaderParams < GridSpec >) => {
< div className = " flex h-full w-full flex-col items-end justify-center tabular-nums " >
< div className = " text-ln-text-light font-mono uppercase " >{ subname }</ div >
Selecting rows by clicking anywhere on the row can conflict with other grid interactions or diverge
from common UX patterns. A common alternative is checkbox-based row selection.
LyteNyte Grid provides the api.rowHandleSelect helper to simplify checkbox selection logic,
including support for shift-based range selection.
Checkbox Row Selection import " @1771technologies/lytenyte-pro/light-dark.css " ;
import type { OrderData } from " @1771technologies/grid-sample-data/orders " ;
import { data } from " @1771technologies/grid-sample-data/orders " ;
} from " ./components.jsx " ;
import { useClientDataSource , Grid } from " @1771technologies/lytenyte-pro " ;
import { Checkbox , SelectAll } from " @1771technologies/lytenyte-pro/components " ;
export interface GridSpec {
readonly data : OrderData ;
const columns : Grid . Column < GridSpec > [] = [
{ id : " id " , width : 60 , widthMin : 60 , cellRenderer : IdCell , name : " ID " },
{ id : " product " , cellRenderer : ProductCell , width : 200 , name : " Product " },
{ id : " price " , type : " number " , cellRenderer : PriceCell , width : 100 , name : " Price " },
{ id : " customer " , cellRenderer : AvatarCell , width : 180 , name : " Customer " },
{ id : " purchaseDate " , cellRenderer : PurchaseDateCell , name : " Purchase Date " , width : 130 },
{ id : " paymentMethod " , cellRenderer : PaymentMethodCell , name : " Payment Method " , width : 150 },
{ id : " email " , cellRenderer : EmailCell , width : 220 , name : " Email " },
const marker : Grid . ColumnMarker < GridSpec > = {
cellRenderer : MarkerCell ,
headerRenderer : MarkerHeader ,
export default function RowDemo () {
const ds = useClientDataSource ( { data : data } ) ;
className = " ln-grid ln-cell-marker:border-e ln-cell-marker:border-e-ln-border-strong "
rowSelectionMode = " multiple "
rowSelectionActivator = " none "
function MarkerHeader ( params : Grid . T . HeaderParams < GridSpec >) {
< div className = " flex h-full w-full items-center justify-center " >
slot = {({ indeterminate , selected , toggle }) => {
indeterminate = { indeterminate }
if ( ev . key === " Enter " || ev . key === " " ) toggle ();
function MarkerCell ({ api , selected } : Grid . T . CellRendererParams < GridSpec >) {
< div className = " flex h-full w-full items-center justify-center " >
api . rowHandleSelect ({ shiftKey: ev . shiftKey , target: ev . target });
if ( ev . key === " Enter " || ev . key === " " )
api . rowHandleSelect ({ shiftKey: ev . shiftKey , target: ev . target });
import { format } from " date-fns " ;
import { type JSX , type ReactNode } from " react " ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import type { GridSpec } from " ./demo.jsx " ;
import { clsx , type ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
export function ProductCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
const url = row . data ?. productThumbnail ;
const title = row . data . product ;
const desc = row . data . productDescription ;
< div className = " flex h-full w-full items-center gap-2 " >
< img className = " border-ln-border-strong h-7 w-7 rounded-lg border " src = { url } alt = { title + desc } />
< div className = " text-ln-text-dark flex flex-col gap-0.5 " >
< div className = " font-semibold " >{ title }</ div >
< div className = " text-ln-text-light text-xs " >{ desc }</ div >
export function AvatarCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
const url = row . data ?. customerAvatar ;
const name = row . data . customer ;
< div className = " flex h-full w-full items-center gap-2 " >
< img className = " border-ln-border-strong h-7 w-7 rounded-full border " src = { url } alt = { name } />
< div className = " text-ln-text-dark flex flex-col gap-0.5 " >
const formatter = new Intl . NumberFormat ( " en-Us " , {
minimumFractionDigits : 2 ,
maximumFractionDigits : 2 ,
export function PriceCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
const price = formatter . format ( row . data . price ) ;
const [ dollars , cents ] = price . split ( " . " ) ;
< div className = " flex h-full w-full items-center justify-end " >
< div className = " flex items-baseline tabular-nums " >
< span className = " text-ln-text font-semibold " >${ dollars }</ span >.
< span className = " relative text-xs " >{ cents }</ span >
export function PurchaseDateCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
const formattedDate = format ( row . data . purchaseDate , " dd MMM, yyyy " ) ;
return < div className = " flex h-full w-full items-center " >{ formattedDate }</ div >;
export function IdCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
return < div className = " text-xs tabular-nums " >{ row . data . id }</ div >;
export function PaymentMethodCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
const cardNumber = row . data . cardNumber ;
const provider = row . data . paymentMethod ;
let Logo : ReactNode = null ;
if ( provider === " Visa " ) Logo = < VisaLogo className = " w-6 " />;
if ( provider === " Mastercard " ) Logo = < MastercardLogo className = " w-6 " />;
< div className = " flex h-full w-full items-center gap-2 " >
< div className = " flex w-7 items-center justify-center " >{ Logo }</ div >
< div className = " flex items-center gap-px " >
< div className = " bg-ln-gray-40 size-2 rounded-full " ></ div >
< div className = " bg-ln-gray-40 size-2 rounded-full " ></ div >
< div className = " bg-ln-gray-40 size-2 rounded-full " ></ div >
< div className = " bg-ln-gray-40 size-2 rounded-full " ></ div >
< div className = " tabular-nums " >{ cardNumber }</ div >
export function EmailCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
return < div className = " text-ln-primary-50 flex h-full w-full items-center " >{ row . data . email }</ div >;
const VisaLogo = ( props : JSX . IntrinsicElements [ " svg " ] ) => (
< svg xmlns = " http://www.w3.org/2000/svg " width = { 2500 } height = { 812 } viewBox = " 0.5 0.5 999 323.684 " { ... props }>
d = " M651.185.5c-70.933 0-134.322 36.766-134.322 104.694 0 77.9 112.423 83.28 112.423 122.415 0 16.478-18.884 31.229-51.137 31.229-45.773 0-79.984-20.611-79.984-20.611l-14.638 68.547s39.41 17.41 91.734 17.41c77.552 0 138.576-38.572 138.576-107.66 0-82.316-112.89-87.537-112.89-123.86 0-12.91 15.501-27.053 47.662-27.053 36.286 0 65.892 14.99 65.892 14.99l14.326-66.204S696.614.5 651.185.5zM2.218 5.497.5 15.49s29.842 5.461 56.719 16.356c34.606 12.492 37.072 19.765 42.9 42.353l63.51 244.832h85.138L379.927 5.497h-84.942L210.707 218.67l-34.39-180.696c-3.154-20.68-19.13-32.477-38.685-32.477H2.218zm411.865 0L347.449 319.03h80.999l66.4-313.534h-80.765zm451.759 0c-19.532 0-29.88 10.457-37.474 28.73L709.699 319.03h84.942l16.434-47.468h103.483l9.994 47.468H999.5L934.115 5.497h-68.273zm11.047 84.707 25.178 117.653h-67.454z "
const MastercardLogo = ( props : JSX . IntrinsicElements [ " svg " ] ) => (
xmlns = " http://www.w3.org/2000/svg "
viewBox = " 55.2 38.3 464.5 287.8 "
d = " M519.7 182.2c0 79.5-64.3 143.9-143.6 143.9s-143.6-64.4-143.6-143.9S296.7 38.3 376 38.3s143.7 64.4 143.7 143.9z "
d = " M342.4 182.2c0 79.5-64.3 143.9-143.6 143.9S55.2 261.7 55.2 182.2 119.5 38.3 198.8 38.3s143.6 64.4 143.6 143.9z "
d = " M287.4 68.9c-33.5 26.3-55 67.3-55 113.3s21.5 87 55 113.3c33.5-26.3 55-67.3 55-113.3s-21.5-86.9-55-113.3z "
The demo uses api.rowHandleSelect to handle checkbox interactions:
api . rowHandleSelect ({ shiftKey: ev . shiftKey , target: ev . target });
if ( ev . key === " Enter " || ev . key === " " )
api . rowHandleSelect ({ shiftKey: ev . shiftKey , target: ev . target });
Checkbox selection and click selection are not mutually exclusive in LyteNyte Grid.
You can use both at the same time. Checkboxes are a great way to indicate to users
that they can select rows.
The selection checkbox demo allows you to select or deselect all rows
in the grid using the header checkbox in the marker column . The demo
uses the SelectAll component . However, you can achieve the same select-all
behavior programmatically with the api.rowSelect method, as shown below:
api . rowSelect ( { selected : " all " } ) ; // select all rows
api . rowSelect ( { selected : " all " , deselect : true } ) ; // deselect all rows
Selecting all rows only works if rowSelectionMode is set to "multiple".
The row selection state is maintained by the row data source provided to the grid.
The selection state determines which rows the grid marks as selected, and which rows are returned
when you request the selected row set.
While each row data source maintains its own selection state in memory,
all LyteNyte Grid data sources use the same row selection state representation.
The explanation below applies to the built-in LyteNyte Grid
data sources. If you use a custom data source, the behavior described here may not apply.
The row selection state may be one of two types:
linked: Used when the row selection mode is "multiple" and when the rowsIsolatedSelection
property is set to false on the data source.
isolated: Used when the row selection mode is "single" or when rowsIsolatedSelection
is set to true on the data source.
The code below shows how to set linked and isolated selection when using the client row data source .
// rowsIsolatedSelection is false by default, so it can be omitted.
const ds = useClientDataSource ( { data : data , rowsIsolatedSelection : false } ) ;
const ds = useClientDataSource ( { data : data , rowsIsolatedSelection : true } ) ;
The different row selection state modes primarily differ in how they handle selecting group rows .
For flat datasets with no grouping, both modes behave identically.
Isolated row selection treats every row as individually selectable. As a TypeScript interface, it
is represented as follows:
interface RowSelectionIsolated {
readonly kind : " isolated " ;
readonly selected : boolean ;
readonly exceptions : Set < string >;
With isolated selection, selecting a group row does not select its children, and selecting all the
child rows of a group does not select the parent. Each row maintains its own unique selection state.
RowSelectionIsolated works as an exclusion set. The selected property determines whether rows
are selected by default. If selected is true, all rows are selected by default. If selected is
false, no rows are selected by default. The exceptions set contains row IDs that invert the
default selection:
If selected is true, any row with an ID in exceptions is not selected.
If selected is false, any row with an ID in exceptions is selected.
The demo below demonstrates isolated row selection. Notice that selecting a group row does not
select its children. This is the intended behavior of isolated row selection.
Isolated Row Selection import " @1771technologies/lytenyte-pro/components.css " ;
import " @1771technologies/lytenyte-pro/light-dark.css " ;
import type { LoanDataItem } from " @1771technologies/grid-sample-data/loan-data " ;
import { loanData } from " @1771technologies/grid-sample-data/loan-data " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
} from " ./components.js " ;
import { Checkbox , RowGroupCell , SelectAll } from " @1771technologies/lytenyte-pro/components " ;
export interface GridSpec {
readonly data : LoanDataItem ;
const columns : Grid . Column < GridSpec > [] = [
{ name : " Name " , id : " name " , cellRenderer : NameCell , width : 110 },
{ name : " Country " , id : " country " , width : 150 , cellRenderer : CountryCell },
{ name : " Loan Amount " , id : " loanAmount " , width : 120 , type : " number " , cellRenderer : NumberCell },
{ name : " Balance " , id : " balance " , type : " number " , cellRenderer : NumberCell },
{ name : " Customer Rating " , id : " customerRating " , type : " number " , width : 125 , cellRenderer : CustomerRating },
{ name : " Marital " , id : " marital " },
{ name : " Education " , id : " education " },
{ name : " Job " , id : " job " , width : 120 },
{ name : " Overdue " , id : " overdue " , cellRenderer : OverdueCell },
{ name : " Duration " , id : " duration " , type : " number " , cellRenderer : DurationCell },
{ name : " Date " , id : " date " , width : 110 , cellRenderer : DateCell },
{ name : " Age " , id : " age " , width : 80 , type : " number " },
{ name : " Contact " , id : " contact " },
const base : Grid . ColumnBase < GridSpec > = { width : 100 };
const group : Grid . RowGroupColumn < GridSpec > = {
cellRenderer : RowGroupCell ,
const marker : Grid . ColumnMarker < GridSpec > = {
cellRenderer : MarkerCell ,
headerRenderer : MarkerHeader ,
export default function RowDemo () {
const ds = useClientDataSource ( {
group : [ { id : " job " }, { id : " education " } ] ,
rowGroupDefaultExpansion : true ,
rowsIsolatedSelection : true ,
< div className = " ln-grid ln-header:data-[ln-colid=overdue]:justify-center " style = {{ height: 500 }}>
rowSelectionMode = " multiple "
function MarkerHeader ( params : Grid . T . HeaderParams < GridSpec >) {
< div className = " flex h-full w-full items-center justify-center " >
slot = {({ indeterminate , selected , toggle }) => {
indeterminate = { indeterminate }
if ( ev . key === " Enter " || ev . key === " " ) toggle ();
function MarkerCell ({ api , selected } : Grid . T . CellRendererParams < GridSpec >) {
< div className = " flex h-full w-full items-center justify-center " >
api . rowHandleSelect ({ shiftKey: ev . shiftKey , target: ev . target });
if ( ev . key === " Enter " || ev . key === " " )
api . rowHandleSelect ({ shiftKey: ev . shiftKey , target: ev . target });
import { type Grid } from " @1771technologies/lytenyte-pro " ;
import type { GridSpec } from " ./demo " ;
import { twMerge } from " tailwind-merge " ;
import clsx , { type ClassValue } from " clsx " ;
import { countryFlags } from " @1771technologies/grid-sample-data/loan-data " ;
import { useId , useMemo } from " react " ;
import { format , isValid , parse } from " date-fns " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
const formatter = new Intl . NumberFormat ( " en-US " , {
maximumFractionDigits : 2 ,
minimumFractionDigits : 0 ,
export function NumberCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return " - " ;
const formatted = field < 0 ? ` -$ ${ formatter . format ( Math . abs ( field )) }` : " $ " + formatter . format ( field ) ;
" flex h-full w-full items-center justify-end tabular-nums " ,
field < 0 && " text-red-600 dark:text-red-300 " ,
field > 0 && " text-green-600 dark:text-green-300 " ,
export function NameCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
const url = row . data ?. avatar ;
const name = row . data . name ;
< div className = " flex h-full w-full items-center gap-2 " >
< img className = " border-ln-border-strong h-7 w-7 rounded-full border " src = { url } alt = { name } />
< div className = " text-ln-text-dark flex flex-col gap-0.5 " >
export function DurationCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
return typeof field === " number " ? `${ formatter . format ( field ) } days ` : `${ field ?? " - "}` ;
export function CountryCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const flag = countryFlags [ field as keyof typeof countryFlags ] ;
< div className = " flex h-full w-full items-center gap-2 " >
< img className = " size-4 " src = { flag } alt = { ` country flag of ${ field }` } />
< span >{ String ( field ?? " - " )}</ span >
export function DateCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( typeof field !== " string " ) return " - " ;
const dateField = parse ( field as string , " yyyy-MM-dd " , new Date ()) ;
if ( ! isValid ( dateField )) return " - " ;
const niceDate = format ( dateField , " yyyy MMM dd " ) ;
return < div className = " flex h-full w-full items-center tabular-nums " >{ niceDate }</ div >;
export function OverdueCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( field !== " Yes " && field !== " No " ) return " - " ;
" flex w-full items-center justify-center rounded-lg py-1 font-bold " ,
field === " No " && " bg-green-500/10 text-green-600 " ,
field === " Yes " && " bg-red-500/10 text-red-400 " ,
export function CustomerRating ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return String ( field ?? " - " ) ;
< div className = " flex justify-center text-yellow-300 " >
< StarRating value = { field } />
export default function StarRating ({ value = 0 } : { value : number }) {
const clamped = useMemo ( () => {
const n = Number . isFinite ( value ) ? value : 0 ;
return Math . max ( 0 , Math . min ( max , n )) ;
const stars = useMemo ( () => {
return Array . from ( { length : max }, ( _ , i ) => {
const fill = Math . max ( 0 , Math . min ( 1 , clamped - i )) ; // 0..1
< div className = { " inline-flex items-center " } role = " img " >
{ stars . map (({ i , fill }) => {
const gradId = `${ uid } -star- ${ i }` ;
return < StarIcon key = { i } fillFraction = { fill } gradientId = { gradId } />;
function StarIcon ({ fillFraction , gradientId } : { fillFraction : number ; gradientId : string }) {
const pct = Math . round ( fillFraction * 100 ) ;
< svg width = " 20 " height = " 20 " viewBox = " 0 0 24 24 " aria-hidden = " true " >
< linearGradient id = { gradientId } x1 = " 0% " y1 = " 0% " x2 = " 100% " y2 = " 0% " >
< stop offset = { `${ pct } % ` } stopColor = " currentColor " />
< stop offset = { `${ pct } % ` } stopColor = " transparent " />
d = " M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z "
fill = { ` url(# ${ gradientId } ) ` }
{ /* Optional outline for crisp edges */ }
d = " M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z "
Linked row selection represents selection state as an inclusion/exclusion tree of nodes. The
type interfaces are shown below. The root of the selection tree is the RowSelectionLinked type.
interface RowSelectNode {
children ?: Map < string , RowSelectNode >;
interface RowSelectionLinked {
readonly selected : boolean ;
readonly children : Map < string , RowSelectNode >;
The selection state of a row is determined by the closest node
in the tree to that row, including the row itself.
Consider the tree below. Node A is not selected, but its child, Node Y ,
is selected, so the selection set includes Node Y . Node X is not
selected because it does not override selection, and its parent is not selected.
On the other hand, since Node B is selected,
Node W and Node U are also selected.
This gives you a selection set that includes Node W and Node U .
Node A (selected = false)
| -- Node Y (selected = true)
The linked row representation may appear complex at first, but it has some very desirable properties:
It compactly represents group selection states, especially when many groups are selected.
It works even when only a subset of rows is loaded, crucial for server
data loading where only some group rows may be available.
It can be serialized into a short string, which is important
for maintaining selection state in your application’s URL.
LyteNyte Grid compacts and collapses redundant nodes in the tree as the user selects rows.
This keeps the tree as flat as possible while still representing the correct selection state.
As a developer, you rarely need to interact with the selection state directly.
In most cases, it is sufficient to call the rowsSelected
method on the data source or the grid API. This method returns
the currently selected row nodes.
The demo below demonstrates linked selection. Selecting all children of a
group row also selects the group row, and selecting the
group row selects all its children.
Linked Row Selection import " @1771technologies/lytenyte-pro/components.css " ;
import " @1771technologies/lytenyte-pro/light-dark.css " ;
import type { LoanDataItem } from " @1771technologies/grid-sample-data/loan-data " ;
import { loanData } from " @1771technologies/grid-sample-data/loan-data " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
} from " ./components.js " ;
import { Checkbox , RowGroupCell , SelectAll } from " @1771technologies/lytenyte-pro/components " ;
export interface GridSpec {
readonly data : LoanDataItem ;
const columns : Grid . Column < GridSpec > [] = [
{ name : " Name " , id : " name " , cellRenderer : NameCell , width : 110 },
{ name : " Country " , id : " country " , width : 150 , cellRenderer : CountryCell },
{ name : " Loan Amount " , id : " loanAmount " , width : 120 , type : " number " , cellRenderer : NumberCell },
{ name : " Balance " , id : " balance " , type : " number " , cellRenderer : NumberCell },
{ name : " Customer Rating " , id : " customerRating " , type : " number " , width : 125 , cellRenderer : CustomerRating },
{ name : " Marital " , id : " marital " },
{ name : " Education " , id : " education " },
{ name : " Job " , id : " job " , width : 120 },
{ name : " Overdue " , id : " overdue " , cellRenderer : OverdueCell },
{ name : " Duration " , id : " duration " , type : " number " , cellRenderer : DurationCell },
{ name : " Date " , id : " date " , width : 110 , cellRenderer : DateCell },
{ name : " Age " , id : " age " , width : 80 , type : " number " },
{ name : " Contact " , id : " contact " },
const base : Grid . ColumnBase < GridSpec > = { width : 100 };
const group : Grid . RowGroupColumn < GridSpec > = {
cellRenderer : RowGroupCell ,
const marker : Grid . ColumnMarker < GridSpec > = {
cellRenderer : MarkerCell ,
headerRenderer : MarkerHeader ,
export default function RowDemo () {
const ds = useClientDataSource ( {
group : [ { id : " job " }, { id : " education " } ] ,
rowGroupDefaultExpansion : true ,
rowsIsolatedSelection : false ,
< div className = " ln-grid ln-header:data-[ln-colid=overdue]:justify-center " style = {{ height: 500 }}>
rowSelectionMode = " multiple "
function MarkerHeader ( params : Grid . T . HeaderParams < GridSpec >) {
< div className = " flex h-full w-full items-center justify-center " >
slot = {({ indeterminate , selected , toggle }) => {
indeterminate = { indeterminate }
if ( ev . key === " Enter " || ev . key === " " ) toggle ();
function MarkerCell ({ api , selected , indeterminate } : Grid . T . CellRendererParams < GridSpec >) {
< div className = " flex h-full w-full items-center justify-center " >
indeterminate = { indeterminate }
api . rowHandleSelect ({ shiftKey: ev . shiftKey , target: ev . target });
if ( ev . key === " Enter " || ev . key === " " )
api . rowHandleSelect ({ shiftKey: ev . shiftKey , target: ev . target });
import { type Grid } from " @1771technologies/lytenyte-pro " ;
import type { GridSpec } from " ./demo " ;
import { twMerge } from " tailwind-merge " ;
import clsx , { type ClassValue } from " clsx " ;
import { countryFlags } from " @1771technologies/grid-sample-data/loan-data " ;
import { useId , useMemo } from " react " ;
import { format , isValid , parse } from " date-fns " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
const formatter = new Intl . NumberFormat ( " en-US " , {
maximumFractionDigits : 2 ,
minimumFractionDigits : 0 ,
export function NumberCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return " - " ;
const formatted = field < 0 ? ` -$ ${ formatter . format ( Math . abs ( field )) }` : " $ " + formatter . format ( field ) ;
" flex h-full w-full items-center justify-end tabular-nums " ,
field < 0 && " text-red-600 dark:text-red-300 " ,
field > 0 && " text-green-600 dark:text-green-300 " ,
export function NameCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return ;
const url = row . data ?. avatar ;
const name = row . data . name ;
< div className = " flex h-full w-full items-center gap-2 " >
< img className = " border-ln-border-strong h-7 w-7 rounded-full border " src = { url } alt = { name } />
< div className = " text-ln-text-dark flex flex-col gap-0.5 " >
export function DurationCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
return typeof field === " number " ? `${ formatter . format ( field ) } days ` : `${ field ?? " - "}` ;
export function CountryCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const flag = countryFlags [ field as keyof typeof countryFlags ] ;
< div className = " flex h-full w-full items-center gap-2 " >
< img className = " size-4 " src = { flag } alt = { ` country flag of ${ field }` } />
< span >{ String ( field ?? " - " )}</ span >
export function DateCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( typeof field !== " string " ) return " - " ;
const dateField = parse ( field as string , " yyyy-MM-dd " , new Date ()) ;
if ( ! isValid ( dateField )) return " - " ;
const niceDate = format ( dateField , " yyyy MMM dd " ) ;
return < div className = " flex h-full w-full items-center tabular-nums " >{ niceDate }</ div >;
export function OverdueCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( field !== " Yes " && field !== " No " ) return " - " ;
" flex w-full items-center justify-center rounded-lg py-1 font-bold " ,
field === " No " && " bg-green-500/10 text-green-600 " ,
field === " Yes " && " bg-red-500/10 text-red-400 " ,
export function CustomerRating ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return String ( field ?? " - " ) ;
< div className = " flex justify-center text-yellow-300 " >
< StarRating value = { field } />
export default function StarRating ({ value = 0 } : { value : number }) {
const clamped = useMemo ( () => {
const n = Number . isFinite ( value ) ? value : 0 ;
return Math . max ( 0 , Math . min ( max , n )) ;
const stars = useMemo ( () => {
return Array . from ( { length : max }, ( _ , i ) => {
const fill = Math . max ( 0 , Math . min ( 1 , clamped - i )) ; // 0..1
< div className = { " inline-flex items-center " } role = " img " >
{ stars . map (({ i , fill }) => {
const gradId = `${ uid } -star- ${ i }` ;
return < StarIcon key = { i } fillFraction = { fill } gradientId = { gradId } />;
function StarIcon ({ fillFraction , gradientId } : { fillFraction : number ; gradientId : string }) {
const pct = Math . round ( fillFraction * 100 ) ;
< svg width = " 20 " height = " 20 " viewBox = " 0 0 24 24 " aria-hidden = " true " >
< linearGradient id = { gradientId } x1 = " 0% " y1 = " 0% " x2 = " 100% " y2 = " 0% " >
< stop offset = { `${ pct } % ` } stopColor = " currentColor " />
< stop offset = { `${ pct } % ` } stopColor = " transparent " />
d = " M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z "
fill = { ` url(# ${ gradientId } ) ` }
{ /* Optional outline for crisp edges */ }
d = " M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z "
Each LyteNyte Grid row data source maintains an internal representation of row selection
state. This internal state is uncontrolled , similar to the value of an <input>
element without an onChange handler. You can control this state by setting the
rowSelection property on the row data source.
To listen for updates, provide an onRowSelectionChange callback. For example:
const [ selection , setSelection ] = useState < Grid . T . RowSelectionLinked > ( {
const ds = useClientDataSource ( {
onRowSelectionChange : setSelection ,
LyteNyte Grid’s row data sources enforce that the row selection state matches the
rowsIsolatedSelection property on the data source. When providing your own row selection
state, ensure that the selection type you pass matches the isolation setting.
All row data sources implement the rowsSelected method. Since this method is implemented
on the row data source, the grid API also makes it accessible. You can call it
from whichever reference is most convenient.
The rowsSelected method returns the selected row nodes for the current row selection state. The
nodes returned depend on the data source. For client-side data sources, such as
the tree data source , all selected rows are available. For server
data sources, only rows that have been loaded from the server are returned.
In the demo below, selecting rows and pressing “Alert Rows” fetches those rows
and triggers an alert. Use this callback to retrieve selected
row data in your application.
Get Selected Rows import " @1771technologies/lytenyte-pro/components.css " ;
import " @1771technologies/lytenyte-pro/light-dark.css " ;
import " @1771technologies/lytenyte-pro/pill-manager.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
PercentCellPositiveNegative ,
} from " ./components.jsx " ;
import type { DEXPerformanceData } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
import { data } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
export interface GridSpec {
readonly data : DEXPerformanceData ;
const columns : Grid . Column < GridSpec > [] = [
{ id : " symbol " , cellRenderer : SymbolCell , width : 250 , name : " Symbol " },
{ id : " network " , cellRenderer : NetworkCell , width : 220 , hide : true , name : " Network " },
{ id : " exchange " , cellRenderer : ExchangeCell , width : 220 , hide : true , name : " Exchange " },
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Change " , " 24h " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 1w " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 1m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 3m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 6m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " YTD " ) ,
{ id : " volatility " , cellRenderer : PercentCell , name : " Volatility " , type : " number " },
cellRenderer : PercentCell ,
headerRenderer : makePerfHeaderCell ( " Volatility " , " 1m " ) ,
const base : Grid . ColumnBase < GridSpec > = { width : 80 };
export default function RowDemo () {
const ds = useClientDataSource ( { data : data } ) ;
< div className = " border-ln-border flex gap-4 border-b px-4 py-3 " >
const selected = ds . rowsSelected ();
if ( ! selected . rows . length ) alert ( " There are currently no rows selected. " );
if ( selected . rows . length === 1 ) alert ( " 1 row selected. " );
if ( selected . rows . length > 1 ) alert ( `${ selected . rows . length } rows selected. ` );
className = " ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight "
rowSelectionMode = " multiple "
rowSelectionActivator = " single-click "
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import { exchanges , networks , symbols } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import type { GridSpec } from " ./demo " ;
export function SymbolCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const ticker = row . data . symbolTicker ;
const symbol = row . data . symbol ;
const image = symbols [ row . data . symbolTicker ] ;
< div className = " grid grid-cols-[20px_auto_auto] items-center gap-1.5 " >
alt = { ` Logo for symbol ${ symbol }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px] " >
< div className = " w-full overflow-hidden text-ellipsis " >{ symbol . split ( " / " )[ 0 ]}</ div >
export function NetworkCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const name = row . data . network ;
const image = networks [ name ] ;
< div className = " grid grid-cols-[20px_1fr] items-center gap-1.5 " >
alt = { ` Logo for network ${ name }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " w-full overflow-hidden text-ellipsis " >{ name }</ div >
export function ExchangeCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const name = row . data . exchange ;
const image = exchanges [ name ] ;
< div className = " grid grid-cols-[20px_1fr] items-center gap-1.5 " >
alt = { ` Logo for exchange ${ name }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " w-full overflow-hidden text-ellipsis " >{ name }</ div >
export function PercentCellPositiveNegative ({ api , column , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return " - " ;
const value = ( field > 0 ? " + " : "" ) + ( field * 100 ) . toFixed ( 2 ) + " % " ;
" h-ful flex w-full items-center justify-end tabular-nums " ,
field < 0 ? " text-red-600 dark:text-red-300 " : " text-green-600 dark:text-green-300 " ,
export function PercentCell ({ api , column , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return " - " ;
const value = ( field > 0 ? " + " : "" ) + ( field * 100 ) . toFixed ( 2 ) + " % " ;
return < div className = " h-ful flex w-full items-center justify-end tabular-nums " >{ value }</ div >;
export const makePerfHeaderCell = ( name : string , subname : string ) => {
return ( _ : Grid . T . HeaderParams < GridSpec >) => {
< div className = " flex h-full w-full flex-col items-end justify-center tabular-nums " >
< div className = " text-ln-text-light font-mono uppercase " >{ subname }</ div >
The Alert Rows button uses the ds.rowsSelected method to retrieve
the selected rows and alert the user with the count, as shown in the code below:
data-ln-button = " tertiary "
const selected = ds . rowsSelected ();
if ( ! selected . rows . length ) alert ( " There are currently no rows selected. " );
if ( selected . rows . length === 1 ) alert ( " 1 row selected. " );
if ( selected . rows . length > 1 ) alert ( `${ selected . rows . length } rows selected. ` );
The grid uses the row data source to handle row selection state,
but selection is triggered through user interactions, such as clicking a row.
LyteNyte Grid fires the onRowSelect callback whenever users select rows in the grid. You
can use the event parameters to prevent selection or perform a different action
that aligns with your application.
For example, use onRowSelect to limit row selection to 3 (or another number), as shown below.
Prevent Row Selection import " @1771technologies/lytenyte-pro/components.css " ;
import " @1771technologies/lytenyte-pro/light-dark.css " ;
import " @1771technologies/lytenyte-pro/pill-manager.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
PercentCellPositiveNegative ,
} from " ./components.jsx " ;
import type { DEXPerformanceData } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
import { data } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
export interface GridSpec {
readonly data : DEXPerformanceData ;
const columns : Grid . Column < GridSpec > [] = [
{ id : " symbol " , cellRenderer : SymbolCell , width : 250 , name : " Symbol " },
{ id : " network " , cellRenderer : NetworkCell , width : 220 , hide : true , name : " Network " },
{ id : " exchange " , cellRenderer : ExchangeCell , width : 220 , hide : true , name : " Exchange " },
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Change " , " 24h " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 1w " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 1m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 3m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 6m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " YTD " ) ,
{ id : " volatility " , cellRenderer : PercentCell , name : " Volatility " , type : " number " },
cellRenderer : PercentCell ,
headerRenderer : makePerfHeaderCell ( " Volatility " , " 1m " ) ,
const base : Grid . ColumnBase < GridSpec > = { width : 80 };
export default function RowDemo () {
const ds = useClientDataSource ( { data : data } ) ;
className = " ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight "
rowSelectionMode = " multiple "
rowSelectionActivator = " single-click "
onRowSelect = {({ preventDefault , rows , deselect , api }) => {
if ( rows === " all " || deselect ) return ;
const current = api . rowsSelected (). rows ;
const finalSet = new Set ([ ... rows , ... current . map (( x ) => x . id )]);
alert ( " You can only select a maximum of three rows " );
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import { exchanges , networks , symbols } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import type { GridSpec } from " ./demo " ;
export function SymbolCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const ticker = row . data . symbolTicker ;
const symbol = row . data . symbol ;
const image = symbols [ row . data . symbolTicker ] ;
< div className = " grid grid-cols-[20px_auto_auto] items-center gap-1.5 " >
alt = { ` Logo for symbol ${ symbol }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px] " >
< div className = " w-full overflow-hidden text-ellipsis " >{ symbol . split ( " / " )[ 0 ]}</ div >
export function NetworkCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const name = row . data . network ;
const image = networks [ name ] ;
< div className = " grid grid-cols-[20px_1fr] items-center gap-1.5 " >
alt = { ` Logo for network ${ name }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " w-full overflow-hidden text-ellipsis " >{ name }</ div >
export function ExchangeCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const name = row . data . exchange ;
const image = exchanges [ name ] ;
< div className = " grid grid-cols-[20px_1fr] items-center gap-1.5 " >
alt = { ` Logo for exchange ${ name }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " w-full overflow-hidden text-ellipsis " >{ name }</ div >
export function PercentCellPositiveNegative ({ api , column , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return " - " ;
const value = ( field > 0 ? " + " : "" ) + ( field * 100 ) . toFixed ( 2 ) + " % " ;
" h-ful flex w-full items-center justify-end tabular-nums " ,
field < 0 ? " text-red-600 dark:text-red-300 " : " text-green-600 dark:text-green-300 " ,
export function PercentCell ({ api , column , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return " - " ;
const value = ( field > 0 ? " + " : "" ) + ( field * 100 ) . toFixed ( 2 ) + " % " ;
return < div className = " h-ful flex w-full items-center justify-end tabular-nums " >{ value }</ div >;
export const makePerfHeaderCell = ( name : string , subname : string ) => {
return ( _ : Grid . T . HeaderParams < GridSpec >) => {
< div className = " flex h-full w-full flex-col items-end justify-center tabular-nums " >
< div className = " text-ln-text-light font-mono uppercase " >{ subname }</ div >
From the demo code, notice that you can call preventDefault to stop the
row selection from happening:
onRowSelect = { ( { preventDefault , rows , deselect , api } ) => {
if ( rows === " all " || deselect ) return ;
const current = api . rowsSelected () . rows ;
const finalSet = new Set ([ ... rows , ... current . map ( ( x ) => x . id )]) ;
alert ( " You can only select a maximum of three rows " ) ;
Each row data source maintains an ID universe , which is
a complete set of all possible row IDs currently available
in that data source.
For client-side data , this is the set of all possible rows for the current source configuration.
For server-side data , this is the set of all loaded rows.
LyteNyte Grid uses the ID universe to ensure selected row IDs are valid,
meaning the ID of a selected row must exist in the grid.
If you want the grid to include row IDs that are not yet created but might
be added later to the data source, you can specify additional IDs
using the rowSelectionIdUniverseAdditions property on the data source, as shown below:
const ds = useClientDataSource ( {
rowSelectionIdUniverseAdditions : [
{ id : " unloaded-row-1 " , root : false },
{ id : " unloaded-row-2 " , root : true },
The root flag indicates that the row exists at the top level of the grid. This flag must be set
and only impacts row selection when there are row groups present.
Only use rowSelectionIdUniverseAdditions when the rows you want to preselect are not
immediately available in the data source.
To exclude rows from the ID universe , use the rowSelectionIdUniverseSubtractions
property, as shown below. Rows excluded from the ID universe are not selectable. This
behavior is useful when a row has a special use case and should not participate in normal
selection behavior.
const ds = useClientDataSource ( {
rowSelectionIdUniverseSubtractions : new Set ([ " row-1 " ]) ,
A row’s selection state is reset when the view changes in a way
that makes existing rows invalid. Common actions that invalidate rows include:
Row grouping
Filtering
Sorting (only for server-side data)
The rowSelectKey property on the row data source can override
the default reset behavior with your own key. It functions similarly
to the dependency array in useMemo or useCallback hooks. Provide an
array of values, and LyteNyte Grid shallow-compares the values whenever the key changes.
In the example below, clicking the Update Key button forces a row selection reset. The demo does this
by setting rowSelectKey to an array containing a count, then incrementing the count whenever the button
is clicked.
Resetting Row Selection import " @1771technologies/lytenyte-pro/components.css " ;
import " @1771technologies/lytenyte-pro/light-dark.css " ;
import " @1771technologies/lytenyte-pro/pill-manager.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
PercentCellPositiveNegative ,
} from " ./components.jsx " ;
import type { DEXPerformanceData } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
import { data } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
import { useState } from " react " ;
export interface GridSpec {
readonly data : DEXPerformanceData ;
const columns : Grid . Column < GridSpec > [] = [
{ id : " symbol " , cellRenderer : SymbolCell , width : 250 , name : " Symbol " },
{ id : " network " , cellRenderer : NetworkCell , width : 220 , hide : true , name : " Network " },
{ id : " exchange " , cellRenderer : ExchangeCell , width : 220 , hide : true , name : " Exchange " },
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Change " , " 24h " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 1w " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 1m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 3m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " 6m " ) ,
cellRenderer : PercentCellPositiveNegative ,
headerRenderer : makePerfHeaderCell ( " Perf % " , " YTD " ) ,
{ id : " volatility " , cellRenderer : PercentCell , name : " Volatility " , type : " number " },
cellRenderer : PercentCell ,
headerRenderer : makePerfHeaderCell ( " Volatility " , " 1m " ) ,
const base : Grid . ColumnBase < GridSpec > = { width : 80 };
export default function RowDemo () {
const [ count , setCount ] = useState ( 0 ) ;
const ds = useClientDataSource ( {
< div className = " border-ln-border flex gap-4 border-b px-4 py-3 " >
setCount (( prev ) => prev + 1 );
className = " ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight "
rowSelectionMode = " multiple "
rowSelectionActivator = " single-click "
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import { exchanges , networks , symbols } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import type { GridSpec } from " ./demo " ;
export function SymbolCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const ticker = row . data . symbolTicker ;
const symbol = row . data . symbol ;
const image = symbols [ row . data . symbolTicker ] ;
< div className = " grid grid-cols-[20px_auto_auto] items-center gap-1.5 " >
alt = { ` Logo for symbol ${ symbol }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px] " >
< div className = " w-full overflow-hidden text-ellipsis " >{ symbol . split ( " / " )[ 0 ]}</ div >
export function NetworkCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const name = row . data . network ;
const image = networks [ name ] ;
< div className = " grid grid-cols-[20px_1fr] items-center gap-1.5 " >
alt = { ` Logo for network ${ name }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " w-full overflow-hidden text-ellipsis " >{ name }</ div >
export function ExchangeCell ({ api , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const name = row . data . exchange ;
const image = exchanges [ name ] ;
< div className = " grid grid-cols-[20px_1fr] items-center gap-1.5 " >
alt = { ` Logo for exchange ${ name }` }
className = " h-full w-full overflow-hidden rounded-full "
< div className = " w-full overflow-hidden text-ellipsis " >{ name }</ div >
export function PercentCellPositiveNegative ({ api , column , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return " - " ;
const value = ( field > 0 ? " + " : "" ) + ( field * 100 ) . toFixed ( 2 ) + " % " ;
" h-ful flex w-full items-center justify-end tabular-nums " ,
field < 0 ? " text-red-600 dark:text-red-300 " : " text-green-600 dark:text-green-300 " ,
export function PercentCell ({ api , column , row } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row ) || ! row . data ) return null ;
const field = api . columnField ( column , row ) ;
if ( typeof field !== " number " ) return " - " ;
const value = ( field > 0 ? " + " : "" ) + ( field * 100 ) . toFixed ( 2 ) + " % " ;
return < div className = " h-ful flex w-full items-center justify-end tabular-nums " >{ value }</ div >;
export const makePerfHeaderCell = ( name : string , subname : string ) => {
return ( _ : Grid . T . HeaderParams < GridSpec >) => {
< div className = " flex h-full w-full flex-col items-end justify-center tabular-nums " >
< div className = " text-ln-text-light font-mono uppercase " >{ subname }</ div >
Row Pinning : Freeze rows at the top or bottom of the viewport. Row Full Width : Create rows that span the full width of the viewport. Row Master Detail : Render expandable row detail content, including nested grids.