Cells
Cell Range Selection Copy Page Open additional page export options
LyteNyte Grid supports selecting single or multiple ranges. A range is a rectangular block of cells that the grid marks as selected.
Set the cellSelectionMode property on the grid to configure the cell selection behavior of
LyteNyte Grid. This property accepts three possible values:
"range": Allows a single range of cells to be selected.
"multi-range": Supports selecting multiple cell ranges. Hold Control or Command to add ranges.
"none": Disables cell selection in the grid. By default, cell selection is disabled.
Set cellSelectionMode to "range" to enable single-range selection. Click a cell,
then drag across the grid to select a range.
Single-Range Selection import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { stockData } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import { PercentCell , CurrencyCell , SymbolCell , CompactNumberCell } from " ./components.jsx " ;
type StockData = ( typeof stockData )[ number ] ;
export interface GridSpec {
readonly data : StockData ;
const columns : Grid . Column < GridSpec > [] = [
{ field : 0 , id : " symbol " , name : " Symbol " , cellRenderer : SymbolCell , width : 220 },
{ field : 3 , id : " price " , type : " number " , name : " Price " , cellRenderer : CurrencyCell , width : 110 },
{ field : 5 , id : " change " , type : " number " , name : " Change " , cellRenderer : PercentCell , width : 130 },
{ field : 11 , id : " eps " , name : " EPS " , type : " number " , cellRenderer : CurrencyCell , width : 130 },
{ field : 6 , id : " volume " , name : " Volume " , type : " number " , cellRenderer : CompactNumberCell , width : 130 },
const base : Grid . ColumnBase < GridSpec > = { widthFlex : 1 };
export default function CellDemo () {
const ds = useClientDataSource ( { data : stockData } ) ;
< div className = " ln-grid ln-cell:text-xs ln-cell:font-light ln-header:text-xs " style = {{ height: 500 }}>
< Grid columns = { columns } columnBase = { base } rowSource = { ds } cellSelectionMode = " range " />
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import { memo } from " react " ;
import { logos } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import type { GridSpec } from " ./demo " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
function CompactNumberCellImpl ({ row , api , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const [ label , suffix ] = typeof field === " number " ? formatCompactNumber ( field ) : [ " - " , "" ] ;
< div className = " flex h-full w-full items-center justify-end gap-1 text-nowrap tabular-nums " >
< span className = " font-semibold " >{ suffix }</ span >
export const CompactNumberCell = memo ( CompactNumberCellImpl ) ;
function formatCompactNumber ( n : number ) {
const suffixes = [ "" , " K " , " M " , " B " , " T " ] ;
while ( num >= 1000 && magnitude < suffixes . length - 1 ) {
const formatted = num . toFixed ( decimals ) ;
return [ `${ n < 0 ? " - " : ""}${ formatted }` , suffixes [ magnitude ]] ;
const formatter = new Intl . NumberFormat ( " en-US " , {
minimumFractionDigits : 2 ,
maximumFractionDigits : 2 ,
function CurrencyCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const label = typeof field === " number " ? formatter . format ( field ) : " - " ;
< div className = " flex h-full w-full items-center justify-end text-nowrap tabular-nums " >
< div className = " flex items-baseline gap-1 " >
< span className = " text-[9px] " >USD</ span >
export const CurrencyCell = memo ( CurrencyCellImpl ) ;
function PercentCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) as number ;
const label = typeof field === " number " ? formatter . format ( field ) + " % " : " - " ;
< div className = { tw ( " flex h-full w-full items-center justify-end text-nowrap tabular-nums " )}>{ label }</ div >
export const PercentCell = memo ( PercentCellImpl ) ;
function SymbolCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row )) return null ;
const symbol = api . columnField ( column , row ) as string ;
const desc = row . data ?. [ 1 ] ;
< div className = " grid h-full w-full grid-cols-[32px_1fr] items-center gap-3 overflow-hidden text-nowrap " >
< div className = " flex h-8 min-h-8 w-8 min-w-8 items-center justify-center overflow-hidden rounded-full " >
className = " h-6.5 min-h-6.5 w-6.5 pointer-events-none min-w-[26] rounded-full bg-black p-1 "
< div className = " overflow-hidden text-ellipsis " >{ desc }</ div >
export const SymbolCell = memo ( SymbolCellImpl ) ;
To enable cell selection, the demo sets the cellSelectionMode property as shown in the code below.
< Grid columns = { columns } columnBase = { base } rowSource = { ds } cellSelectionMode = " range " />
When cell selection is enabled, LyteNyte Grid tracks the selection state
internally. Call cellSelections to retrieve the current selection. Uncontrolled cell selection
works well for operations like copy-to-clipboard without managing selection state in your app.
In the demo below, select a cell range and press Control+C or Command+C to copy to your clipboard.
Copy-to-Clipboard import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { stockData } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import { PercentCell , CurrencyCell , SymbolCell , CompactNumberCell } from " ./components.jsx " ;
import { useMemo } from " react " ;
type StockData = ( typeof stockData )[ number ] ;
export interface GridSpec {
readonly data : StockData ;
const columns : Grid . Column < GridSpec > [] = [
{ field : 0 , id : " symbol " , name : " Symbol " , cellRenderer : SymbolCell , width : 220 },
{ field : 3 , id : " price " , type : " number " , name : " Price " , cellRenderer : CurrencyCell , width : 110 },
{ field : 5 , id : " change " , type : " number " , name : " Change " , cellRenderer : PercentCell , width : 130 },
{ field : 11 , id : " eps " , name : " EPS " , type : " number " , cellRenderer : CurrencyCell , width : 130 },
{ field : 6 , id : " volume " , name : " Volume " , type : " number " , cellRenderer : CompactNumberCell , width : 130 },
const base : Grid . ColumnBase < GridSpec > = { widthFlex : 1 };
export default function CellDemo () {
const ds = useClientDataSource ( { data : stockData } ) ;
< div className = " ln-grid ln-cell:text-xs ln-cell:font-light ln-header:text-xs " style = {{ height: 500 }}>
cellSelectionMode = " range "
events = { useMemo < Grid . Events < GridSpec >>(
keyDown : async ({ event : ev , viewport : vp , api }) => {
if ( ev . key === " c " && ( ev . metaKey || ev . ctrlKey )) {
const rect = api . cellSelections ()?.[ 0 ];
const v = await api . exportData ({ rect });
vp . classList . add ( " copy-flash " );
const asString = v . data . map (( x ) => `${ x . join ( " , " ) }` ). join ( " \n " );
await navigator . clipboard . writeText ( asString );
setTimeout (() => vp . classList . remove ( " copy-flash " ), 500 );
background-color : var ( --color-ln-primary-10 );
background-color : var ( --color-ln-primary-30 );
background-color : var ( --color-ln-primary-10 );
& [ data-ln-cell-selection-rect =" true "] : not([data-ln-cell-selection-is-pivot= " true " ]) {
animation : flash 0.5 s ease-out forwards ;
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import { memo } from " react " ;
import { logos } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import type { GridSpec } from " ./demo " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
function CompactNumberCellImpl ({ row , api , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const [ label , suffix ] = typeof field === " number " ? formatCompactNumber ( field ) : [ " - " , "" ] ;
< div className = " flex h-full w-full items-center justify-end gap-1 text-nowrap tabular-nums " >
< span className = " font-semibold " >{ suffix }</ span >
export const CompactNumberCell = memo ( CompactNumberCellImpl ) ;
function formatCompactNumber ( n : number ) {
const suffixes = [ "" , " K " , " M " , " B " , " T " ] ;
while ( num >= 1000 && magnitude < suffixes . length - 1 ) {
const formatted = num . toFixed ( decimals ) ;
return [ `${ n < 0 ? " - " : ""}${ formatted }` , suffixes [ magnitude ]] ;
const formatter = new Intl . NumberFormat ( " en-US " , {
minimumFractionDigits : 2 ,
maximumFractionDigits : 2 ,
function CurrencyCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const label = typeof field === " number " ? formatter . format ( field ) : " - " ;
< div className = " flex h-full w-full items-center justify-end text-nowrap tabular-nums " >
< div className = " flex items-baseline gap-1 " >
< span className = " text-[9px] " >USD</ span >
export const CurrencyCell = memo ( CurrencyCellImpl ) ;
function PercentCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) as number ;
const label = typeof field === " number " ? formatter . format ( field ) + " % " : " - " ;
< div className = { tw ( " flex h-full w-full items-center justify-end text-nowrap tabular-nums " )}>{ label }</ div >
export const PercentCell = memo ( PercentCellImpl ) ;
function SymbolCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row )) return null ;
const symbol = api . columnField ( column , row ) as string ;
const desc = row . data ?. [ 1 ] ;
< div className = " grid h-full w-full grid-cols-[32px_1fr] items-center gap-3 overflow-hidden text-nowrap " >
< div className = " flex h-8 min-h-8 w-8 min-w-8 items-center justify-center overflow-hidden rounded-full " >
className = " h-6.5 min-h-6.5 w-6.5 pointer-events-none min-w-[26] rounded-full bg-black p-1 "
< div className = " overflow-hidden text-ellipsis " >{ desc }</ div >
export const SymbolCell = memo ( SymbolCellImpl ) ;
The demo listens for the viewport keyDown event, then:
Retrieves the current selection bounds via api.cellSelections.
Exports the selected range data using api.exportData, converts it to a string, and writes it to the clipboard.
Applies copy-flash to highlight the selection.
The implementation code is shown below.
cellSelectionMode = " range "
events = { useMemo < Grid . Events < GridSpec >>(
keyDown : async ( ev , vp , api ) => {
if ( ev . key === " c " && ( ev . metaKey || ev . ctrlKey )) {
const rect = api . cellSelections ()?.[ 0 ];
const v = await api . exportData ({ rect });
vp . classList . add ( " copy-flash " );
const asString = v . data . map (( x ) => `${ x . join ( " , " ) }` ). join ( " \n " );
await navigator . clipboard . writeText ( asString );
setTimeout (() => vp . classList . remove ( " copy-flash " ), 1000 );
Some use cases require you to manage the cell selection state. Enable
controlled selection by passing an array of ranges to the cellSelections
prop. Update the selection state via the onCellSelectionChange callback.
The demo below stores cellSelections in React state using
useState. It passes the selection to a status bar that
displays averages for numeric columns.
Selection Status Bar import " @1771technologies/lytenyte-pro/light-dark.css " ;
} from " @1771technologies/lytenyte-pro " ;
import { stockData } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
} from " ./components.jsx " ;
import { useMemo , useState } from " react " ;
import { sum } from " es-toolkit " ;
type StockData = ( typeof stockData )[ number ] ;
export interface GridSpec {
readonly data : StockData ;
const columns : Grid . Column < GridSpec > [] = [
{ field : 0 , id : " symbol " , name : " Symbol " , cellRenderer : SymbolCell , width : 220 },
{ field : 3 , id : " price " , type : " number " , name : " Price " , cellRenderer : CurrencyCell , width : 110 },
{ field : 5 , id : " change " , type : " number " , name : " Change " , cellRenderer : PercentCell , width : 130 },
{ field : 11 , id : " eps " , name : " EPS " , type : " number " , cellRenderer : CurrencyCell , width : 130 },
{ field : 6 , id : " volume " , name : " Volume " , type : " number " , cellRenderer : CompactNumberCell , width : 130 },
const base : Grid . ColumnBase < GridSpec > = { widthFlex : 1 };
export default function CellDemo () {
const ds = useClientDataSource ( { data : stockData } ) ;
const [ rect , setRect ] = useState < Grid . T . DataRect [] > ([
{ rowStart : 1 , rowEnd : 4 , columnStart : 1 , columnEnd : 2 },
< div className = " ln-grid ln-cell:text-xs ln-cell:font-light ln-header:text-xs " style = {{ height: 500 }}>
cellSelectionMode = " range "
onCellSelectionChange = { setRect }
< div className = " border-ln-border flex h-fit items-center border-t " >
{ rect . length > 0 && < StatusBar rect = { rect [ 0 ]} source = { ds } />}
function StatusBar ({ rect , source } : { rect : Grid . T . DataRect ; source : RowSourceClient }) {
const selectedNumberColumns = useMemo ( () => {
const selected : Grid . Column < GridSpec > [] = [] ;
for ( let i = rect . columnStart ; i < rect . columnEnd ; i ++ ) {
if ( col . type === " number " ) selected . push ( col ) ;
}, [ rect . columnEnd , rect . columnStart ]) ;
const summedValues = useMemo ( () => {
const result = Object . fromEntries ( selectedNumberColumns . map ( ( x ) => [ x . id , []])) as Record <
for ( let i = rect . rowStart ; i < rect . rowEnd ; i ++ ) {
const row = source . rowByIndex ( i ) . get () ;
for ( const c of selectedNumberColumns ) {
const value = computeField ( c . field ?? c . id , row ) ;
if ( typeof value === " number " ) result [ c . id ] . push ( value ) ;
}, [ rect . rowEnd , rect . rowStart , selectedNumberColumns , source ]) ;
if ( selectedNumberColumns . length === 0 ) return null ;
< div className = " flex w-full flex-wrap items-center justify-center gap-4 px-2 py-2 " >
{ Object . entries ( summedValues ). map (([ id , values ]) => {
const avg = sum ( values ) / values . length ;
const column = columns . find (( x ) => x . id === id ) ! ;
< div className = " flex items-center justify-center gap-2 text-xs " key = { id }>
< span className = " text-ln-text-dark text-nowrap font-semibold " >
Avg. { column ?. name ?? column . id }:
{( id === " price " || id === " eps " ) && < CurrencyLabel value = { avg } />}
{ id === " change " && < PercentLabel value = { avg } />}
{ id === " volume " && < CompactNumberLabel value = { avg } />}
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import { memo } from " react " ;
import { logos } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import type { GridSpec } from " ./demo " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
function CompactNumberCellImpl ({ row , api , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
return < CompactNumberLabel value = { field as string } />;
export function CompactNumberLabel ({ value } : { value : string | number }) {
const [ label , suffix ] = typeof value === " number " ? formatCompactNumber ( value ) : [ " - " , "" ] ;
< div className = " flex h-full w-full items-center justify-end gap-1 text-nowrap tabular-nums " >
< span className = " font-semibold " >{ suffix }</ span >
export const CompactNumberCell = memo ( CompactNumberCellImpl ) ;
function formatCompactNumber ( n : number ) {
const suffixes = [ "" , " K " , " M " , " B " , " T " ] ;
while ( num >= 1000 && magnitude < suffixes . length - 1 ) {
const formatted = num . toFixed ( decimals ) ;
return [ `${ n < 0 ? " - " : ""}${ formatted }` , suffixes [ magnitude ]] ;
const formatter = new Intl . NumberFormat ( " en-US " , {
minimumFractionDigits : 2 ,
maximumFractionDigits : 2 ,
function CurrencyCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const label = typeof field === " number " ? formatter . format ( field ) : " - " ;
< div className = " flex h-full w-full items-center justify-end text-nowrap tabular-nums " >
< CurrencyLabel value = { label } />
export function CurrencyLabel ({ value } : { value : number | string }) {
< div className = " flex items-baseline gap-1 " >
< span >{ typeof value === " number " ? formatter . format ( value ) : value }</ span >
< span className = " text-[9px] " >USD</ span >
export const CurrencyCell = memo ( CurrencyCellImpl ) ;
function PercentCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) as number ;
return < PercentLabel value = { field } />;
export function PercentLabel ({ value } : { value : string | number }) {
const label = typeof value === " number " ? formatter . format ( value ) + " % " : " - " ;
< div className = { tw ( " flex h-full w-full items-center justify-end text-nowrap tabular-nums " )}>{ label }</ div >
export const PercentCell = memo ( PercentCellImpl ) ;
function SymbolCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row )) return null ;
const symbol = api . columnField ( column , row ) as string ;
const desc = row . data ?. [ 1 ] ;
< div className = " text-ln-text-dark grid h-full w-full grid-cols-[32px_1fr] items-center gap-3 overflow-hidden text-nowrap " >
< div className = " flex h-8 min-h-8 w-8 min-w-8 items-center justify-center overflow-hidden rounded-full " >
className = " h-6.5 min-h-6.5 w-6.5 pointer-events-none min-w-[26] rounded-full bg-black p-1 "
< div className = " overflow-hidden text-ellipsis " >{ desc }</ div >
export const SymbolCell = memo ( SymbolCellImpl ) ;
The cellSelections property expects an array of DataRect objects. A DataRect defines a rectangular
selection area using the following interface. The rowEnd and columnEnd values are exclusive and are
not included in the selected cells.
readonly rowStart : number ;
readonly columnStart : number ;
readonly columnEnd : number ;
cellSelections refers to both an API method and a grid property :
API Method (api.cellSelections): Returns the current selection rectangles.
Property (cellSelections): Controls the selection state.
In controlled selection mode, api.cellSelections returns the same value you pass to cellSelections.
Headers of columns containing selected cells receive the
data-ln-cell-selected="true" attribute. Use this attribute to style
those headers. In the demo below, CSS highlights any header
whose column contains a selected cell.
Column Header Highlights import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { stockData } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import { PercentCell , CurrencyCell , SymbolCell , CompactNumberCell } from " ./components.jsx " ;
type StockData = ( typeof stockData )[ number ] ;
export interface GridSpec {
readonly data : StockData ;
const columns : Grid . Column < GridSpec > [] = [
{ field : 0 , id : " symbol " , name : " Symbol " , cellRenderer : SymbolCell , width : 220 },
{ field : 3 , id : " price " , type : " number " , name : " Price " , cellRenderer : CurrencyCell , width : 110 },
{ field : 5 , id : " change " , type : " number " , name : " Change " , cellRenderer : PercentCell , width : 130 },
{ field : 11 , id : " eps " , name : " EPS " , type : " number " , cellRenderer : CurrencyCell , width : 130 },
{ field : 6 , id : " volume " , name : " Volume " , type : " number " , cellRenderer : CompactNumberCell , width : 130 },
const base : Grid . ColumnBase < GridSpec > = { widthFlex : 1 };
export default function CellDemo () {
const ds = useClientDataSource ( { data : stockData } ) ;
" ln-grid ln-cell:text-xs ln-cell:font-light ln-header:text-xs ln-cell-marker:border-e ln-cell-marker:border-ln-border-strong " ,
" dark:ln-header:data-[ln-cell-selected=true]:bg-[#161D2A] " ,
" ln-header:data-[ln-cell-selected=true]:bg-[#f2f7ff] " ,
< Grid columns = { columns } columnBase = { base } rowSource = { ds } cellSelectionMode = " range " />
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import { memo } from " react " ;
import { logos } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import type { GridSpec } from " ./demo " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
function CompactNumberCellImpl ({ row , api , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const [ label , suffix ] = typeof field === " number " ? formatCompactNumber ( field ) : [ " - " , "" ] ;
< div className = " flex h-full w-full items-center justify-end gap-1 text-nowrap tabular-nums " >
< span className = " font-semibold " >{ suffix }</ span >
export const CompactNumberCell = memo ( CompactNumberCellImpl ) ;
function formatCompactNumber ( n : number ) {
const suffixes = [ "" , " K " , " M " , " B " , " T " ] ;
while ( num >= 1000 && magnitude < suffixes . length - 1 ) {
const formatted = num . toFixed ( decimals ) ;
return [ `${ n < 0 ? " - " : ""}${ formatted }` , suffixes [ magnitude ]] ;
const formatter = new Intl . NumberFormat ( " en-US " , {
minimumFractionDigits : 2 ,
maximumFractionDigits : 2 ,
function CurrencyCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const label = typeof field === " number " ? formatter . format ( field ) : " - " ;
< div className = " flex h-full w-full items-center justify-end text-nowrap tabular-nums " >
< div className = " flex items-baseline gap-1 " >
< span className = " text-[9px] " >USD</ span >
export const CurrencyCell = memo ( CurrencyCellImpl ) ;
function PercentCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) as number ;
const label = typeof field === " number " ? formatter . format ( field ) + " % " : " - " ;
< div className = { tw ( " flex h-full w-full items-center justify-end text-nowrap tabular-nums " )}>{ label }</ div >
export const PercentCell = memo ( PercentCellImpl ) ;
function SymbolCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row )) return null ;
const symbol = api . columnField ( column , row ) as string ;
const desc = row . data ?. [ 1 ] ;
< div className = " grid h-full w-full grid-cols-[32px_1fr] items-center gap-3 overflow-hidden text-nowrap " >
< div className = " flex h-8 min-h-8 w-8 min-w-8 items-center justify-center overflow-hidden rounded-full " >
className = " h-6.5 min-h-6.5 w-6.5 pointer-events-none min-w-[26] rounded-full bg-black p-1 "
< div className = " overflow-hidden text-ellipsis " >{ desc }</ div >
export const SymbolCell = memo ( SymbolCellImpl ) ;
The demo uses the LyteNyte Grid Tailwind plugin to apply the following
Tailwind CSS class:
"ln-header:data-[ln-cell-selected=true]:bg-ln-primary-05"
If you are not using Tailwind, the class is equivalent to the CSS below:
[ data-ln-header-cell =" true "][ data-ln-cell-selected =" true "] {
background-color : var ( --ln-primary-05 );
The marker column is an internal column that is automatically
pinned to the start of the grid. It does not appear in the columns array you pass to the grid.
By default, selection ranges can include the marker column.
Set cellSelectionExcludeMarker to true to prevent selection of this column.
LyteNyte Grid also applies the data-ln-cell-selected attribute to rows that contain selected
cells. Use the attribute for row-level styling.
Exclude Marker Column import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { stockData } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import { PercentCell , CurrencyCell , SymbolCell , CompactNumberCell } from " ./components.jsx " ;
type StockData = ( typeof stockData )[ number ] ;
export interface GridSpec {
readonly data : StockData ;
const columns : Grid . Column < GridSpec > [] = [
{ field : 0 , id : " symbol " , name : " Symbol " , cellRenderer : SymbolCell , width : 220 },
{ field : 3 , id : " price " , type : " number " , name : " Price " , cellRenderer : CurrencyCell , width : 110 },
{ field : 5 , id : " change " , type : " number " , name : " Change " , cellRenderer : PercentCell , width : 130 },
{ field : 11 , id : " eps " , name : " EPS " , type : " number " , cellRenderer : CurrencyCell , width : 130 },
{ field : 6 , id : " volume " , name : " Volume " , type : " number " , cellRenderer : CompactNumberCell , width : 130 },
const base : Grid . ColumnBase < GridSpec > = { widthFlex : 1 };
const marker : Grid . ColumnMarker < GridSpec > = {
return < div className = " flex h-full w-full items-center justify-center text-xs " >{ p . rowIndex + 1 }</ div >;
export default function CellDemo () {
const ds = useClientDataSource ( { data : stockData } ) ;
" ln-grid ln-cell:text-xs ln-cell:font-light " ,
" ln-row:data-[ln-cell-selected=true]:ln-cell-marker:bg-[#f2f7ff] " ,
" dark:ln-row:data-[ln-cell-selected=true]:ln-cell-marker:bg-[#161D2A] " ,
" dark:ln-header:data-[ln-cell-selected=true]:bg-[#161D2A] " ,
" ln-header:data-[ln-cell-selected=true]:bg-[#f2f7ff] " ,
cellSelectionMode = " range "
cellSelectionExcludeMarker
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import { memo } from " react " ;
import { logos } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import type { GridSpec } from " ./demo " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
function CompactNumberCellImpl ({ row , api , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const [ label , suffix ] = typeof field === " number " ? formatCompactNumber ( field ) : [ " - " , "" ] ;
< div className = " flex h-full w-full items-center justify-end gap-1 text-nowrap tabular-nums " >
< span className = " font-semibold " >{ suffix }</ span >
export const CompactNumberCell = memo ( CompactNumberCellImpl ) ;
function formatCompactNumber ( n : number ) {
const suffixes = [ "" , " K " , " M " , " B " , " T " ] ;
while ( num >= 1000 && magnitude < suffixes . length - 1 ) {
const formatted = num . toFixed ( decimals ) ;
return [ `${ n < 0 ? " - " : ""}${ formatted }` , suffixes [ magnitude ]] ;
const formatter = new Intl . NumberFormat ( " en-US " , {
minimumFractionDigits : 2 ,
maximumFractionDigits : 2 ,
function CurrencyCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const label = typeof field === " number " ? formatter . format ( field ) : " - " ;
< div className = " flex h-full w-full items-center justify-end text-nowrap tabular-nums " >
< div className = " flex items-baseline gap-1 " >
< span className = " text-[9px] " >USD</ span >
export const CurrencyCell = memo ( CurrencyCellImpl ) ;
function PercentCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) as number ;
const label = typeof field === " number " ? formatter . format ( field ) + " % " : " - " ;
< div className = { tw ( " flex h-full w-full items-center justify-end text-nowrap tabular-nums " )}>{ label }</ div >
export const PercentCell = memo ( PercentCellImpl ) ;
function SymbolCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row )) return null ;
const symbol = api . columnField ( column , row ) as string ;
const desc = row . data ?. [ 1 ] ;
< div className = " grid h-full w-full grid-cols-[32px_1fr] items-center gap-3 overflow-hidden text-nowrap " >
< div className = " flex h-8 min-h-8 w-8 min-w-8 items-center justify-center overflow-hidden rounded-full " >
className = " h-6.5 min-h-6.5 w-6.5 pointer-events-none min-w-[26] rounded-full bg-black p-1 "
< div className = " overflow-hidden text-ellipsis " >{ desc }</ div >
export const SymbolCell = memo ( SymbolCellImpl ) ;
Even with cellSelectionExcludeMarker set to true, the grid still reserves space for
the marker column. When the marker column is enabled, it
uses column index 0, so the first non-marker column always has column index 1.
To select an entire row or column, set the cellSelections prop to a selection rectangle
that covers the target cells. This works regardless of cellSelectionMode. The demo below illustrates this:
Column Selection: Click a header to select the entire column.
Row Selection: Click a cell in the marker column to select an entire row.
By default, LyteNyte Grid clears selection when focus moves to a
non-cell element, such as a header cell. Set cellSelectionMaintainOnNonCellPosition
to preserve selection when headers receive focus.
Column / Row Selection import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { stockData } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import { PercentCell , CurrencyCell , SymbolCell , CompactNumberCell } from " ./components.jsx " ;
import { useMemo , useState } from " react " ;
type StockData = ( typeof stockData )[ number ] ;
export interface GridSpec {
readonly data : StockData ;
const columns : Grid . Column < GridSpec > [] = [
{ field : 0 , id : " symbol " , name : " Symbol " , cellRenderer : SymbolCell , width : 220 },
{ field : 3 , id : " price " , type : " number " , name : " Price " , cellRenderer : CurrencyCell , width : 110 },
{ field : 5 , id : " change " , type : " number " , name : " Change " , cellRenderer : PercentCell , width : 130 },
{ field : 11 , id : " eps " , name : " EPS " , type : " number " , cellRenderer : CurrencyCell , width : 130 },
{ field : 6 , id : " volume " , name : " Volume " , type : " number " , cellRenderer : CompactNumberCell , width : 130 },
const base : Grid . ColumnBase < GridSpec > = { widthFlex : 1 };
const marker : Grid . ColumnMarker < GridSpec > = {
return < div className = " flex h-full w-full items-center justify-center text-xs " >{ p . rowIndex + 1 }</ div >;
export default function CellDemo () {
const [ cellSelections , setCellSelection ] = useState < Grid . T . DataRect [] > ([]) ;
const ds = useClientDataSource ( { data : stockData } ) ;
" ln-grid ln-cell:text-xs ln-cell:font-light " ,
" ln-row:data-[ln-cell-selected=true]:ln-cell-marker:bg-[#f2f7ff] " ,
" dark:ln-row:data-[ln-cell-selected=true]:ln-cell-marker:bg-[#161D2A] " ,
" dark:ln-header:data-[ln-cell-selected=true]:bg-[#161D2A] " ,
" ln-header:data-[ln-cell-selected=true]:bg-[#f2f7ff] " ,
cellSelectionExcludeMarker
cellSelections = { cellSelections }
cellSelectionMode = " range "
cellSelectionMaintainOnNonCellPosition
onCellSelectionChange = { setCellSelection }
events = { useMemo < Grid . Events < GridSpec >>(
pointerDown : ({ column , layout , api }) => {
if ( column . id !== " lytenyte-marker-column " ) return ;
const view = api . columnView ();
columnEnd: view . visibleColumns . length ,
rowStart: layout . rowIndex ,
rowEnd: layout . rowIndex + 1 ,
pointerDown : ({ column , layout , api }) => {
if ( column . id === " lytenyte-marker-column " ) return ;
const index = layout . colStart ;
const rowView = api . rowView ();
{ columnStart: index , columnEnd: index + 1 , rowStart: 0 , rowEnd: rowView . rowCount },
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import { memo } from " react " ;
import { logos } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import type { GridSpec } from " ./demo " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
function CompactNumberCellImpl ({ row , api , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const [ label , suffix ] = typeof field === " number " ? formatCompactNumber ( field ) : [ " - " , "" ] ;
< div className = " flex h-full w-full items-center justify-end gap-1 text-nowrap tabular-nums " >
< span className = " font-semibold " >{ suffix }</ span >
export const CompactNumberCell = memo ( CompactNumberCellImpl ) ;
function formatCompactNumber ( n : number ) {
const suffixes = [ "" , " K " , " M " , " B " , " T " ] ;
while ( num >= 1000 && magnitude < suffixes . length - 1 ) {
const formatted = num . toFixed ( decimals ) ;
return [ `${ n < 0 ? " - " : ""}${ formatted }` , suffixes [ magnitude ]] ;
const formatter = new Intl . NumberFormat ( " en-US " , {
minimumFractionDigits : 2 ,
maximumFractionDigits : 2 ,
function CurrencyCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const label = typeof field === " number " ? formatter . format ( field ) : " - " ;
< div className = " flex h-full w-full items-center justify-end text-nowrap tabular-nums " >
< div className = " flex items-baseline gap-1 " >
< span className = " text-[9px] " >USD</ span >
export const CurrencyCell = memo ( CurrencyCellImpl ) ;
function PercentCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) as number ;
const label = typeof field === " number " ? formatter . format ( field ) + " % " : " - " ;
< div className = { tw ( " flex h-full w-full items-center justify-end text-nowrap tabular-nums " )}>{ label }</ div >
export const PercentCell = memo ( PercentCellImpl ) ;
function SymbolCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row )) return null ;
const symbol = api . columnField ( column , row ) as string ;
const desc = row . data ?. [ 1 ] ;
< div className = " grid h-full w-full grid-cols-[32px_1fr] items-center gap-3 overflow-hidden text-nowrap " >
< div className = " flex h-8 min-h-8 w-8 min-w-8 items-center justify-center overflow-hidden rounded-full " >
className = " h-6.5 min-h-6.5 w-6.5 pointer-events-none min-w-[26] rounded-full bg-black p-1 "
< div className = " overflow-hidden text-ellipsis " >{ desc }</ div >
export const SymbolCell = memo ( SymbolCellImpl ) ;
When a selection includes a spanning cell, LyteNyte Grid expands the selection rectangle
to cover the cell’s full row/column span. In the demo below, selecting a
spanning cell expands the selected area accordingly.
Cell Selection With Spans 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 as rawData } from " @1771technologies/grid-sample-data/dex-pairs-performance " ;
export interface GridSpec {
readonly data : DEXPerformanceData ;
const exchangeCounts : Record < string , number > = {};
const data = Object . values (
Object . groupBy ( rawData , ( x ) => {
exchangeCounts [ x ! [ 0 ] . exchange ] = Math . min ( x ! . length , 5 ) ;
return x ! . slice ( 0 , 5 ) ; // Only take the first 5
} ) as DEXPerformanceData [] ;
const columns : Grid . Column < GridSpec > [] = [
cellRenderer : SymbolCell ,
const exchange = r . row . data ?. exchange as string ;
return exchangeCounts [ exchange ] ?? 1 ;
{ 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 CellDemo () {
const ds = useClientDataSource ( { data : data } ) ;
className = " ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight "
< Grid columns = { columns } columnBase = { base } rowSource = { ds } cellSelectionMode = " range " />
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 >
Rows and columns can be pinned to the edges of the grid,
keeping them visible while the rest of the grid scrolls.
When you drag a selection from the scrollable area toward a pinned area,
the grid includes pinned cells only after the viewport reaches the
pinned edge. This ensures the selection range remains continuous:
Pinned Start/Top: The grid must be scrolled to the start or top.
Pinned End/Bottom: The grid must be scrolled to the end or bottom.
As you drag toward the edge, LyteNyte Grid automatically scrolls the viewport.
Once the viewport reaches the pinned edge, the selection expands to include
the pinned cells. These constraints only apply when the selection begins
outside the pinned area.
The demo includes pinned rows at the top and bottom, and
columns at the start and end. Select different areas to see
how selection expands into pinned regions.
Pinned Cell Selection import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { stockData } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import { PercentCell , CurrencyCell , SymbolCell , CompactNumberCell } from " ./components.jsx " ;
import { ViewportShadows } from " @1771technologies/lytenyte-pro/components " ;
type StockData = ( typeof stockData )[ number ] ;
export interface GridSpec {
readonly data : StockData ;
const columns : Grid . Column < GridSpec > [] = [
{ field : 0 , id : " symbol " , name : " Symbol " , cellRenderer : SymbolCell , width : 220 , pin : " start " },
{ field : 3 , id : " price " , type : " number " , name : " Price " , cellRenderer : CurrencyCell , width : 200 },
{ field : 5 , id : " change " , type : " number " , name : " Change " , cellRenderer : PercentCell , width : 200 },
{ field : 11 , id : " eps " , name : " EPS " , type : " number " , cellRenderer : CurrencyCell , width : 200 },
cellRenderer : CompactNumberCell ,
const base : Grid . ColumnBase < GridSpec > = { widthFlex : 1 };
export default function CellDemo () {
const ds = useClientDataSource ( {
data : stockData . slice ( 2 , - 2 ) ,
topData : stockData . slice ( 0 , 2 ) ,
botData : stockData . slice ( - 2 ) ,
< div className = " ln-grid ln-cell:text-xs ln-cell:font-light ln-header:text-xs " style = {{ height: 500 }}>
cellSelectionMode = " range "
slotShadows = { ViewportShadows }
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import { memo } from " react " ;
import { logos } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import type { GridSpec } from " ./demo " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
function CompactNumberCellImpl ({ row , api , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const [ label , suffix ] = typeof field === " number " ? formatCompactNumber ( field ) : [ " - " , "" ] ;
< div className = " flex h-full w-full items-center justify-end gap-1 text-nowrap tabular-nums " >
< span className = " font-semibold " >{ suffix }</ span >
export const CompactNumberCell = memo ( CompactNumberCellImpl ) ;
function formatCompactNumber ( n : number ) {
const suffixes = [ "" , " K " , " M " , " B " , " T " ] ;
while ( num >= 1000 && magnitude < suffixes . length - 1 ) {
const formatted = num . toFixed ( decimals ) ;
return [ `${ n < 0 ? " - " : ""}${ formatted }` , suffixes [ magnitude ]] ;
const formatter = new Intl . NumberFormat ( " en-US " , {
minimumFractionDigits : 2 ,
maximumFractionDigits : 2 ,
function CurrencyCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const label = typeof field === " number " ? formatter . format ( field ) : " - " ;
< div className = " flex h-full w-full items-center justify-end text-nowrap tabular-nums " >
< div className = " flex items-baseline gap-1 " >
< span className = " text-[9px] " >USD</ span >
export const CurrencyCell = memo ( CurrencyCellImpl ) ;
function PercentCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) as number ;
const label = typeof field === " number " ? formatter . format ( field ) + " % " : " - " ;
< div className = { tw ( " flex h-full w-full items-center justify-end text-nowrap tabular-nums " )}>{ label }</ div >
export const PercentCell = memo ( PercentCellImpl ) ;
function SymbolCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row )) return null ;
const symbol = api . columnField ( column , row ) as string ;
const desc = row . data ?. [ 1 ] ;
< div className = " grid h-full w-full grid-cols-[32px_1fr] items-center gap-3 overflow-hidden text-nowrap " >
< div className = " flex h-8 min-h-8 w-8 min-w-8 items-center justify-center overflow-hidden rounded-full " >
className = " h-6.5 min-h-6.5 w-6.5 pointer-events-none min-w-[26] rounded-full bg-black p-1 "
< div className = " overflow-hidden text-ellipsis " >{ desc }</ div >
export const SymbolCell = memo ( SymbolCellImpl ) ;
When a selection spans pinned areas, the selection rectangle appears
visually split. This split indicates that the selection crosses both pinned
and scrollable viewport regions. Despite the visual separation, the cellSelections
state contains a single, continuous selection rectangle.
Set cellSelectionMode to "multi-range" to enable multiple selection ranges.
Add Range: Hold Control (or Command ) and drag to add a new range.
Deselect Area: Hold Control (or Command ) and start a selection
inside an existing range to deselect that area.
Multiple Range Selections import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { stockData } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import { PercentCell , CurrencyCell , SymbolCell , CompactNumberCell } from " ./components.jsx " ;
type StockData = ( typeof stockData )[ number ] ;
export interface GridSpec {
readonly data : StockData ;
const columns : Grid . Column < GridSpec > [] = [
{ field : 0 , id : " symbol " , name : " Symbol " , cellRenderer : SymbolCell , width : 220 },
{ field : 3 , id : " price " , type : " number " , name : " Price " , cellRenderer : CurrencyCell , width : 110 },
{ field : 5 , id : " change " , type : " number " , name : " Change " , cellRenderer : PercentCell , width : 130 },
{ field : 11 , id : " eps " , name : " EPS " , type : " number " , cellRenderer : CurrencyCell , width : 130 },
{ field : 6 , id : " volume " , name : " Volume " , type : " number " , cellRenderer : CompactNumberCell , width : 130 },
const base : Grid . ColumnBase < GridSpec > = { widthFlex : 1 };
export default function CellDemo () {
const ds = useClientDataSource ( { data : stockData } ) ;
< div className = " ln-grid ln-cell:text-xs ln-cell:font-light ln-header:text-xs " style = {{ height: 500 }}>
< Grid columns = { columns } columnBase = { base } rowSource = { ds } cellSelectionMode = " multi-range " />
import type { Grid } from " @1771technologies/lytenyte-pro " ;
import { memo } from " react " ;
import { logos } from " @1771technologies/grid-sample-data/stock-data-smaller " ;
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
import type { GridSpec } from " ./demo " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
function CompactNumberCellImpl ({ row , api , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const [ label , suffix ] = typeof field === " number " ? formatCompactNumber ( field ) : [ " - " , "" ] ;
< div className = " flex h-full w-full items-center justify-end gap-1 text-nowrap tabular-nums " >
< span className = " font-semibold " >{ suffix }</ span >
export const CompactNumberCell = memo ( CompactNumberCellImpl ) ;
function formatCompactNumber ( n : number ) {
const suffixes = [ "" , " K " , " M " , " B " , " T " ] ;
while ( num >= 1000 && magnitude < suffixes . length - 1 ) {
const formatted = num . toFixed ( decimals ) ;
return [ `${ n < 0 ? " - " : ""}${ formatted }` , suffixes [ magnitude ]] ;
const formatter = new Intl . NumberFormat ( " en-US " , {
minimumFractionDigits : 2 ,
maximumFractionDigits : 2 ,
function CurrencyCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
const label = typeof field === " number " ? formatter . format ( field ) : " - " ;
< div className = " flex h-full w-full items-center justify-end text-nowrap tabular-nums " >
< div className = " flex items-baseline gap-1 " >
< span className = " text-[9px] " >USD</ span >
export const CurrencyCell = memo ( CurrencyCellImpl ) ;
function PercentCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) as number ;
const label = typeof field === " number " ? formatter . format ( field ) + " % " : " - " ;
< div className = { tw ( " flex h-full w-full items-center justify-end text-nowrap tabular-nums " )}>{ label }</ div >
export const PercentCell = memo ( PercentCellImpl ) ;
function SymbolCellImpl ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
if ( ! api . rowIsLeaf ( row )) return null ;
const symbol = api . columnField ( column , row ) as string ;
const desc = row . data ?. [ 1 ] ;
< div className = " grid h-full w-full grid-cols-[32px_1fr] items-center gap-3 overflow-hidden text-nowrap " >
< div className = " flex h-8 min-h-8 w-8 min-w-8 items-center justify-center overflow-hidden rounded-full " >
className = " h-6.5 min-h-6.5 w-6.5 pointer-events-none min-w-[26] rounded-full bg-black p-1 "
< div className = " overflow-hidden text-ellipsis " >{ desc }</ div >
export const SymbolCell = memo ( SymbolCellImpl ) ;
Multiple ranges are useful for comparing values across separate cell groups.
However, they complicate operations like copying. When multiple ranges exist,
the cellSelections state stores multiple selection rectangles,
which can make it unclear which range to copy.
Use "range" mode unless your application specifically requires multiple ranges.
LyteNyte Grid renders inert div overlays to visualize selections.
These elements are unstyled by default. To style selection rectangles, target their data attributes.
The CSS below illustrates the approach used by built-in themes.
For more information, refer to the Grid Theming guide.
[ data-ln-cell-selection-rect ] : not ([ data-ln-cell-selection-is-unit = " true " ]) {
background-color : var ( --ln-primary-10 );
&[ data-ln-cell-selection-is-deselect =" true "] {
background-color : var ( --ln-red-30 );
&[ data-ln-cell-selection-border-top =" true "],
&[ data-ln-cell-selection-border-bottom =" true "],
&[ data-ln-cell-selection-border-start =" true "],
&[ data-ln-cell-selection-border-end =" true "] {
border-color : var ( --ln-red-50 );
& [ data-ln-cell-selection-border-top = " true " ] {
border-top : 1 px solid var ( --ln-primary-50 );
& [ data-ln-cell-selection-border-bottom = " true " ] {
border-bottom : 1 px solid var ( --ln-primary-50 );
& [ data-ln-cell-selection-border-start = " true " ] {
border-inline-start : 1 px solid var ( --ln-primary-50 );
& [ data-ln-cell-selection-border-end = " true " ] {
border-inline-end : 1 px solid var ( --ln-primary-50 );