Theming
Grid Theming Copy Page Open additional page export options
LyteNyte Grid is a headless data grid. This guide explains how to theme LyteNyte Grid using vanilla CSS or pre-built themes to create a visually polished data grid.
As a headless data grid, LyteNyte Grid applies no colors, fonts, borders, or backgrounds by default.
However, it does include inline styles required for layout calculations and row or column virtualization.
These functional styles should not be overridden. Because inline styles have high specificity,
they are unlikely to be affected unless you deliberately create an extremely specific CSS selector.
The fastest way to create a visually polished grid is by using one of the built-in LyteNyte Grid themes.
To apply a theme, add the ln-grid class to any HTML element above the grid, then
include the class name of the theme you want to use. Ensure you have imported the CSS file
for the themes:
import " @1771technologies/lytenyte-core/grid-full.css " ;
import " @1771technologies/lytenyte-pro/grid-full.css " ;
LyteNyte Grid provides seven ready-to-use themes. Add the theme class, alongside the
ln-grid class, to a parent element of the grid (typically html or body):
ln-teal: Teal-accent theme.
ln-term: Terminal-style color theme.
ln-dark: Standard dark theme.
ln-light: Standard light theme.
ln-shadcn: A theme based on the Shadcn color tokens. Supports both light and dark mode. Use in projects that adopt shadcn.
ln-cotton-candy: Playful, colorful theme for friendly data grid views.
The demo below shows how each theme looks in practice. For a complete example with
auxiliary components, visit our live demo page,
which includes a theme toggle.
Pre-built LyteNyte Grid Themes Theme:
Light Dark LyteNyte Teal Term 256 Shadcn Dark Shadcn Light Cotton Candy
import " @1771technologies/lytenyte-pro/light-dark.css " ;
import " @1771technologies/lytenyte-pro/grid-full.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 { ThemePicker , tw } from " ./theme.jsx " ;
import { useState } from " react " ;
import { ViewportShadows } 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 " },
export default function ThemingDemo () {
const [ selections , setSelections ] = useState < Grid . T . DataRect [] > ([
{ rowStart : 1 , rowEnd : 3 , columnStart : 1 , columnEnd : 3 },
const ds = useClientDataSource ( { data : data } ) ;
const [ theme , setTheme ] = useState ( " ln-dark " ) ;
< div className = " bg-ln-gray-00 border-b-ln-border h-full w-full border-b py-2 " >
< ThemePicker theme = { theme } setTheme = { setTheme } />
className = { tw ( " ln-grid " , theme )}
colorScheme: theme . includes ( " light " ) || theme === " ln-cotton-candy " ? " light " : " dark " ,
slotShadows = { ViewportShadows }
cellSelections = { selections }
onCellSelectionChange = { setSelections }
cellSelectionMode = " range "
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 " ;
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 "
import { ToggleGroup as TG } from " radix-ui " ;
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
export function ToggleGroup ( props : Parameters < typeof TG . Root > [ 0 ] ) {
className = { tw ( " bg-ln-gray-20 flex items-center gap-2 rounded-xl px-2 py-1 " , props . className )}
export function ToggleItem ( props : Parameters < typeof TG . Item > [ 0 ] ) {
" text-ln-text flex cursor-pointer items-center justify-center px-2 py-1 text-xs font-bold outline-none focus:outline-none " ,
" data-[state=on]:text-ln-text-dark data-[state=on]:bg-linear-to-b from-ln-gray-02 to-ln-gray-05 data-[state=on]:rounded-md " ,
export function ThemePicker ({ theme , setTheme } : { theme : string ; setTheme : ( s : string ) => void }) {
< div className = { tw ( " flex h-full items-center gap-1 text-nowrap px-2 py-1 " )}>
< div className = { tw ( " text-light hidden text-xs font-medium md:block " )}>Theme:</ div >
className = { tw ( " flex flex-wrap " )}
< ToggleItem value = " ln-light " >Light</ ToggleItem >
< ToggleItem value = " ln-dark " >Dark</ ToggleItem >
< ToggleItem value = " ln-teal " >LyteNyte Teal</ ToggleItem >
< ToggleItem value = " ln-term " >Term 256</ ToggleItem >
< ToggleItem value = " ln-shadcn dark " >Shadcn Dark</ ToggleItem >
< ToggleItem value = " ln-shadcn light " >Shadcn Light</ ToggleItem >
< ToggleItem value = " ln-cotton-candy " >Cotton Candy</ ToggleItem >
You can also create a custom theme from scratch. LyteNyte Grid is unopinionated about
your styling method and works with any approach.
LyteNyte Grid aims to minimize the final bundle size produced by your application, including the size of your bundled CSS.
For this reason, the LyteNyte Grid packages export individual CSS themes so that unused themes can be excluded from your final CSS bundle.
The exports are shown below with a comment explaining their purpose.
Contains the design theme token definitions for LyteNyte Grid, excluding colors.
These tokens are used for spacing, fonts, and shadows.
@import " @1771technologies/lytenyte-pro/design.css " ;
Contains the base grid styles. This export styles grid elements using the colors
and tokens defined for the grid's theme.
@import " @1771technologies/lytenyte-pro/grid.css " ;
These imports define the various color themes. Import them individually
to include only the colors for a specific theme.
@import " @1771technologies/lytenyte-pro/cotton-candy.css " ;
@import " @1771technologies/lytenyte-pro/dark.css " ;
@import " @1771technologies/lytenyte-pro/light.css " ;
@import " @1771technologies/lytenyte-pro/shadcn.css " ;
@import " @1771technologies/lytenyte-pro/teal.css " ;
@import " @1771technologies/lytenyte-pro/term.css " ;
Imports all predefined theme colors. Use this when you want to include
all pre-made color themes in your application.
@import " @1771technologies/lytenyte-pro/all-colors.css " ;
Imports all tokens, colors, and grid styles. This single import contains
all pre-made CSS provided by LyteNyte Grid.
@import " @1771technologies/lytenyte-pro/grid-full.css " ;
Contains the colors for the ln-light and ln-dark themes, but exports the light
colors under the :root selector and the dark colors under the .dark class.
Useful for a standard light/dark theme setup.
@import " @1771technologies/lytenyte-pro/light-dark.css " ;
The pre-built LyteNyte themes use a set of design tokens to maintain a consistent look and feel.
These tokens are CSS variables that your application can modify to augment or adjust the existing themes.
See the design.css file
in our GitHub repository for the definitions of the non-color design tokens.
See the ln-dark.css file
in our GitHub repository for the definitions of the color design tokens. All themes use the same set of color token names.
The rest of this guide explains how to style the grid using vanilla CSS and inline styles.
Even if you use a framework like Tailwind, reviewing these examples will help
you understand the styling attributes available in LyteNyte Grid.
LyteNyte Grid makes extensive use of data attributes on grid elements.
Data attributes are custom pieces of information attached to an HTML element.
They always begin with the data- prefix. For example, each cell element includes the
attribute data-ln-cell="true". All LyteNyte Grid data attributes begin with the data-ln
prefix, where ln stands for LyteNyte. Using these attributes is an effective
way to target specific parts of the grid for CSS styling.
In the demo below, the header cells and grid cells are styled using HSLA color codes and
the light-dark
CSS function, which is useful for specifying theme colors for light and dark color schemes.
Data Attribute Styling import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { bankDataSmall } from " @1771technologies/grid-sample-data/bank-data-smaller " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
const columns : Grid . Column < GridSpec > [] = [
{ name : " Job " , id : " job " , width : 120 },
{ name : " Age " , id : " age " , type : " number " , width : 80 , cellRenderer : NumberCell },
{ name : " Balance " , id : " balance " , type : " number " , cellRenderer : BalanceCell },
{ name : " Education " , id : " education " },
{ name : " Marital " , id : " marital " },
{ name : " Default " , id : " default " },
{ name : " Housing " , id : " housing " },
{ name : " Loan " , id : " loan " },
{ name : " Contact " , id : " contact " },
{ name : " Day " , id : " day " , type : " number " , cellRenderer : NumberCell },
{ name : " Month " , id : " month " },
{ name : " Duration " , id : " duration " , type : " number " , cellRenderer : DurationCell },
const base : Grid . ColumnBase < GridSpec > = { width : 100 };
export default function ThemingDemo () {
const ds = useClientDataSource ( { data : bankDataSmall } ) ;
< div className = " data-styles " >
< div style = {{ height: 500 }}>
< Grid rowSource = { ds } columns = { columns } columnBase = { base } />
export type BankData = ( typeof bankDataSmall )[ number ] ;
const formatter = new Intl . NumberFormat ( " en-US " , {
maximumFractionDigits : 2 ,
minimumFractionDigits : 0 ,
export function BalanceCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( typeof field === " number " ) {
if ( field < 0 ) return ` -$ ${ formatter . format ( Math . abs ( field )) }` ;
return " $ " + formatter . format ( field ) ;
return `${ field ?? " - "}` ;
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 NumberCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
return typeof field === " number " ? formatter . format ( field ) : `${ field ?? " - "}` ;
[ data-ln-type =" number "] {
background-color : light-dark( white , hsla ( 190 , 32 % , 6 % , 1 ) ) ;
color : light-dark( hsla ( 175 , 6 % , 38 % , 1 ), hsla ( 175 , 10 % , 86 % , 1 ) ) ;
border-bottom : 1 px solid light-dark( hsla ( 175 , 20 % , 95 % , 1 ), hsla ( 177 , 19 % , 17 % , 1 ) ) ;
[ data-ln-alternate = " true " ] [ data-ln-cell = " true " ] {
background-color : light-dark( hsl ( 0 , 27 % , 98 % ), hsl ( 184 , 33 % , 8 % ) ) ;
[ data-ln-header-cell = " true " ] {
background-color : light-dark( hsla ( 175 , 12 % , 92 % , 1 ), hsla ( 177 , 19 % , 17 % , 1 ) ) ;
color : light-dark( hsla ( 177 , 19 % , 17 % , 1 ), hsla ( 175 , 12 % , 92 % , 1 ) ) ;
text-transform : capitalize ;
The CSS uses the attribute selector ,
as shown below:
[ data-ln-type =" number "] {
background-color : light-dark( white , hsla ( 190 , 32 % , 6 % , 1 ) ) ;
color : light-dark( hsla ( 175 , 6 % , 38 % , 1 ), hsla ( 175 , 10 % , 86 % , 1 ) ) ;
border-bottom : 1 px solid light-dark( hsla ( 175 , 20 % , 95 % , 1 ), hsla ( 177 , 19 % , 17 % , 1 ) ) ;
[ data-ln-alternate = " true " ] [ data-ln-cell = " true " ] {
background-color : light-dark( hsl ( 0 , 27 % , 98 % ), hsl ( 184 , 33 % , 8 % ) ) ;
[ data-ln-header-cell = " true " ] {
background-color : light-dark( hsla ( 175 , 12 % , 92 % , 1 ), hsla ( 177 , 19 % , 17 % , 1 ) ) ;
color : light-dark( hsla ( 177 , 19 % , 17 % , 1 ), hsla ( 175 , 12 % , 92 % , 1 ) ) ;
text-transform : capitalize ;
Wrap the styles in a data-styles class to apply them on a per-grid basis.
If you prefer to apply them globally, remove the wrapper class.
It is useful to understand which data attributes apply to each element.
You can inspect the rendered grid in your browser’s developer tools, but
the most common attributes are listed below for convenience.
Each header element includes an attribute indicating its type:
Grid.Viewport: data-ln-viewport
Grid.Header: data-ln-header
Grid.HeaderRow: data-ln-header-row
Grid.HeaderGroupCell: data-ln-header-group
Grid.HeaderCell: data-ln-header-cell
Additionally, Grid.HeaderCell may include data-ln-header-floating
to indicate that the cell belongs to the floating header.
Some elements also include attributes for contextual styling. For example,
Grid.HeaderCell includes data-ln-colpin to indicate the pin
section (start, center, or end). It may also include data-ln-last-start-pin
or data-ln-first-end-pin to indicate whether the cell is the last in the start section or the first in the end section.
Grid.HeaderGroupCell includes attributes describing its state. The
data-ln-collapsible attribute indicates that the group cell can collapse,
while data-ln-collapsed reflects the current collapse state.
Use these attributes to precisely target header elements for custom styling.
Each row and cell element includes an identifying data attribute:
Grid.RowsContainer: data-ln-rows-container
Grid.RowsTop: data-ln-rows-top
Grid.RowsCenter: data-ln-rows-center
Grid.RowsBottom: data-ln-rows-bottom
Grid.Row: data-ln-row
Grid.RowFullWidth: data-ln-row and data-ln-rowtype="full-width"
Grid.Cell: data-ln-cell
Additional contextual attributes support conditional styling. For example,
Grid.Row may include data-ln-last-top-pin or data-ln-first-bottom-pin
to indicate whether the row is the last top-pinned or first bottom-pinned row.
Using these attributes provides a consistent and reliable way to
target specific parts of LyteNyte Grid for styling.
LyteNyte Grid provides flexible styling options. Since each component is exposed,
you can apply any number of classes to its elements. Use this approach when you
have an existing design system and want LyteNyte Grid to match it.
Styling With Classes import { bankDataSmall } from " @1771technologies/grid-sample-data/bank-data-smaller " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
export type BankData = ( typeof bankDataSmall )[ number ] ;
const columns : Grid . Column < GridSpec > [] = [
{ name : " Job " , id : " job " , width : 120 },
{ name : " Age " , id : " age " , type : " number " , width : 80 , cellRenderer : NumberCell },
{ name : " Balance " , id : " balance " , type : " number " , cellRenderer : BalanceCell },
{ name : " Education " , id : " education " },
{ name : " Marital " , id : " marital " },
{ name : " Default " , id : " default " },
{ name : " Housing " , id : " housing " },
{ name : " Loan " , id : " loan " },
{ name : " Contact " , id : " contact " },
{ name : " Day " , id : " day " , type : " number " , cellRenderer : NumberCell },
{ name : " Month " , id : " month " },
{ name : " Duration " , id : " duration " , type : " number " , cellRenderer : DurationCell },
const base : Grid . ColumnBase < GridSpec > = { width : 100 };
export default function ThemingDemo () {
const ds = useClientDataSource ( { data : bankDataSmall } ) ;
< div className = " classes " >
< div style = {{ height: 500 }}>
< Grid rowSource = { ds } columns = { columns } columnBase = { base }>
if ( cell . kind === " group " ) return null ;
return < Grid.HeaderCell key = { cell . id } cell = { cell } className = " header-cell " />;
if ( row . kind === " full-width " ) return null ;
{ row . cells . map (( cell ) => {
return < Grid.Cell cell = { cell } key = { cell . id } className = " cell " />;
const formatter = new Intl . NumberFormat ( " en-US " , {
maximumFractionDigits : 2 ,
minimumFractionDigits : 0 ,
export function BalanceCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( typeof field === " number " ) {
if ( field < 0 ) return ` -$ ${ formatter . format ( Math . abs ( field )) }` ;
return " $ " + formatter . format ( field ) ;
return `${ field ?? " - "}` ;
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 NumberCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
return typeof field === " number " ? formatter . format ( field ) : `${ field ?? " - "}` ;
background-color : light-dark( white , hsla ( 190 , 32 % , 6 % , 1 ) ) ;
color : light-dark( hsla ( 175 , 6 % , 38 % , 1 ), hsla ( 175 , 10 % , 86 % , 1 ) ) ;
border-bottom : 1 px solid light-dark( hsla ( 175 , 20 % , 95 % , 1 ), hsla ( 177 , 19 % , 17 % , 1 ) ) ;
&[ data-ln-type =" number "] {
justify-content : flex-end ;
[ data-ln-alternate = " true " ] . cell {
background-color : light-dark( hsl ( 0 , 27 % , 98 % ), hsl ( 184 , 33 % , 8 % ) ) ;
background-color : light-dark( hsla ( 175 , 12 % , 92 % , 1 ), hsla ( 177 , 19 % , 17 % , 1 ) ) ;
color : light-dark( hsla ( 177 , 19 % , 17 % , 1 ), hsla ( 175 , 12 % , 92 % , 1 ) ) ;
text-transform : capitalize ;
&[ data-ln-type =" number "] {
justify-content : flex-end ;
The CSS is straightforward. The example below shows how the classes
are applied through the className property on the components.
background-color : light-dark( white , hsla ( 190 , 32 % , 6 % , 1 ) ) ;
color : light-dark( hsla ( 175 , 6 % , 38 % , 1 ), hsla ( 175 , 10 % , 86 % , 1 ) ) ;
border-bottom : 1 px solid light-dark( hsla ( 175 , 20 % , 95 % , 1 ), hsla ( 177 , 19 % , 17 % , 1 ) ) ;
&[ data-ln-type =" number "] {
justify-content : flex-end ;
[ data-ln-alternate = " true " ] . cell {
background-color : light-dark( hsl ( 0 , 27 % , 98 % ), hsl ( 184 , 33 % , 8 % ) ) ;
background-color : light-dark( hsla ( 175 , 12 % , 92 % , 1 ), hsla ( 177 , 19 % , 17 % , 1 ) ) ;
color : light-dark( hsla ( 177 , 19 % , 17 % , 1 ), hsla ( 175 , 12 % , 92 % , 1 ) ) ;
text-transform : capitalize ;
&[ data-ln-type =" number "] {
justify-content : flex-end ;
You can also style LyteNyte Grid elements using inline styles. This approach works
well when style values are dynamic and available in JavaScript. For example, a simple
theme editor can be built this way, although it is not generally recommended.
Inline Styles Cell Background Cell Text Header Background Header Text import { bankDataSmall } from " @1771technologies/grid-sample-data/bank-data-smaller " ;
import { useState } from " react " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
export type BankData = ( typeof bankDataSmall )[ number ] ;
const columns : Grid . Column < GridSpec > [] = [
{ name : " Job " , id : " job " , width : 120 },
{ name : " Age " , id : " age " , type : " number " , width : 80 , cellRenderer : NumberCell },
{ name : " Balance " , id : " balance " , type : " number " , cellRenderer : BalanceCell },
{ name : " Education " , id : " education " },
{ name : " Marital " , id : " marital " },
{ name : " Default " , id : " default " },
{ name : " Housing " , id : " housing " },
{ name : " Loan " , id : " loan " },
{ name : " Contact " , id : " contact " },
{ name : " Day " , id : " day " , type : " number " , cellRenderer : NumberCell },
{ name : " Month " , id : " month " },
{ name : " Duration " , id : " duration " , type : " number " , cellRenderer : DurationCell },
const base : Grid . ColumnBase < GridSpec > = { width : 100 };
export default function ThemingDemo () {
const ds = useClientDataSource ( { data : bankDataSmall } ) ;
const [ cellBg , setCellBg ] = useState ( " #0a1314 " ) ;
const [ cellFg , setCellFg ] = useState ( " #d8dfde " ) ;
const [ headerBg , setHeaderBg ] = useState ( " #233433 " ) ;
const [ headerFg , setHeaderFg ] = useState ( " #e8eded " ) ;
< div className = " flex flex-col gap-2 px-2 pb-4 pt-2 " >
< div className = " grid grid-cols-2 gap-2 2xl:grid-cols-4 2xl:gap-8 " >
< label className = " flex flex-col " >
< span className = " pl-1 text-sm font-bold " >Cell Background</ span >
< div className = " flex items-center gap-2 rounded-lg border border-gray-400 px-2 py-1 dark:border-gray-200 " >
className = " h-6 w-6 border-transparent shadow-none "
onChange = {( e ) => setCellBg ( e . target . value )}
< div className = " text-sm " >{ cellBg }</ div >
< label className = " flex flex-col " >
< span className = " px-1 text-sm font-bold " >Cell Text</ span >
< div className = " flex items-center gap-2 rounded-lg border border-gray-400 px-2 py-1 dark:border-gray-200 " >
className = " h-6 w-6 border-transparent shadow-none "
onChange = {( e ) => setCellFg ( e . target . value )}
< div className = " text-sm " >{ cellFg }</ div >
< label className = " flex flex-col " >
< span className = " pl-1 text-sm font-bold " >Header Background</ span >
< div className = " flex items-center gap-2 rounded-lg border border-gray-400 px-2 py-1 dark:border-gray-200 " >
className = " h-6 w-6 border-transparent shadow-none "
onChange = {( e ) => setHeaderBg ( e . target . value )}
< div className = " text-sm " >{ headerBg }</ div >
< label className = " flex flex-col " >
< span className = " pl-1 text-sm font-bold " >Header Text</ span >
< div className = " flex items-center gap-2 rounded-lg border border-gray-400 px-2 py-1 dark:border-gray-200 " >
className = " h-6 w-6 border-transparent shadow-none "
onChange = {( e ) => setHeaderFg ( e . target . value )}
< div className = " text-sm " >{ headerFg }</ div >
< div style = {{ height: 500 }}>
< Grid rowSource = { ds } columns = { columns } columnBase = { base }>
if ( cell . kind === " group " ) return null ;
justifyContent: cell . type === " number " ? " flex-end " : " flex-start " ,
textTransform: " capitalize " ,
if ( row . kind === " full-width " ) return null ;
{ row . cells . map (( cell ) => {
justifyContent: cell . type === " number " ? " flex-end " : " flex-start " ,
borderBottom: " 1px solid hsla(177, 19%, 17%, 1) " ,
const formatter = new Intl . NumberFormat ( " en-US " , {
maximumFractionDigits : 2 ,
minimumFractionDigits : 0 ,
export function BalanceCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( typeof field === " number " ) {
if ( field < 0 ) return ` -$ ${ formatter . format ( Math . abs ( field )) }` ;
return " $ " + formatter . format ( field ) ;
return `${ field ?? " - "}` ;
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 NumberCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
return typeof field === " number " ? formatter . format ( field ) : `${ field ?? " - "}` ;
Inline styles are applied directly to each component. For example, the Grid.Cell
component can use the style property as shown below.
justifyContent: cell . type === " number " ? " flex-end " : " flex-start " ,
borderBottom: " 1px solid light-dark(hsla(175, 20%, 95%, 1), hsla(177, 19%, 17%, 1)) " ,
Use caution when applying inline styles. The same maintainability issues and limitations of
inline styles also apply in LyteNyte Grid. When used selectively,
inline styles can simplify code, but avoid using them for every grid element.
Most LyteNyte Grid components are accessible through the headless interface.
However, for convenience, some advanced features are rendered
and controlled internally by LyteNyte Grid. These elements include:
The row detail container (master detail container).
The cell selection rectangles.
To style these elements, you must target them directly using CSS.
In most cases, a single selector is enough to apply your styles. If you
are using one of our pre-built themes, these elements are already styled.
The example below shows a grid with a single row detail expanded. Styles
for the detail container are applied by targeting the row element
that includes the data-ln-row-detail attribute.
Styling Row Detail import { companiesWithPricePerf } from " @1771technologies/grid-sample-data/companies-with-price-performance " ;
import { useMemo } from " react " ;
import { Area , AreaChart , CartesianGrid , ResponsiveContainer , XAxis , YAxis } from " recharts " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
type PerformanceData = ( typeof companiesWithPricePerf )[ number ] ;
export interface GridSpec {
readonly data : PerformanceData ;
const columns : Grid . Column < GridSpec > [] = [
{ id : " Company " , widthFlex : 2 },
{ id : " Country " , widthFlex : 2 },
{ id : " Founded " , type : " number " },
{ id : " Employee Cnt " , name : " Employees " , type : " number " , cellRenderer : NumberCell },
{ id : " Price " , type : " number " , cellRenderer : PriceCell },
const rowDetailExpansions = new Set ([ " leaf-0 " ]) ;
const rowDetailRenderer : Grid . Props < GridSpec > [ " rowDetailRenderer " ] = ( p ) => {
if ( ! p . api . rowIsLeaf ( p . row )) return ;
padding: " 20px 20px 20px 0px " ,
< PriceChart row = { p . row } />
const base : Grid . ColumnBase < GridSpec > = { width : 100 , widthFlex : 1 };
export default function ThemingDemo () {
const ds = useClientDataSource ( {
data : companiesWithPricePerf ,
< div style = {{ height: 500 }} className = " detail-styles " >
rowDetailRenderer = { rowDetailRenderer }
rowDetailExpansions = { rowDetailExpansions }
function PriceChart ({ row } : { row : Grid . T . RowLeaf < GridSpec [ " data " ] > }) {
const data = useMemo ( () => {
if ( ! row . data ) return [] ;
const weeks : Record < string , { week : number ; [ key : string ] : number }> = Object . fromEntries (
Array . from ( { length : 52 }, ( _ , i ) => [ i + 1 , { week : i + 1 } ]) ,
const data = row . data [ " 1 Year Perf " ] ;
data . forEach ( ( dp , i ) => {
weeks [ i + 1 ][ row . id ] = dp ;
return Object . values ( weeks ) . sort ( ( l , r ) => l . week - r . week ) ;
< ResponsiveContainer height = " 100% " width = " 100% " >
< linearGradient key = { row . id } id = { row . id } x1 = " 0 " y1 = " 0 " x2 = " 0 " y2 = " 1 " >
< stop offset = " 5% " stopColor = { color . stop5 } stopOpacity = { 0.8 } />
< stop offset = " 95% " stopColor = { color . stop95 } stopOpacity = { 0 } />
ticks = {[ 5 , 10 , 15 , 20 , 25 , 30 , 35 , 40 , 45 , 50 ]}
< YAxis fontFamily = " Inter " fontSize = " 14px " tickLine = { false } axisLine = { false } />
< CartesianGrid vertical = { false } stroke = " var(--ln-border) " />
const formatter = new Intl . NumberFormat ( " en-US " , {
maximumFractionDigits : 2 ,
minimumFractionDigits : 0 ,
export function PriceCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( typeof field === " number " ) {
if ( field < 0 ) return ` -$ ${ formatter . format ( Math . abs ( field )) }` ;
return " $ " + formatter . format ( field ) ;
return `${ field ?? " - "}` ;
export function NumberCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
return typeof field === " number " ? formatter . format ( field ) : `${ field ?? " - "}` ;
background-color : light-dark( white , hsla ( 190 , 32 % , 6 % , 1 ) ) ;
color : light-dark( hsla ( 175 , 6 % , 38 % , 1 ), hsla ( 175 , 10 % , 86 % , 1 ) ) ;
border-bottom : 1 px solid light-dark( hsla ( 175 , 20 % , 95 % , 1 ), hsla ( 177 , 19 % , 17 % , 1 ) ) ;
&[ data-ln-type =" number "] {
[ data-ln-alternate = " true " ] [ data-ln-cell = " true " ] {
background-color : light-dark( hsl ( 0 , 27 % , 98 % ), hsl ( 184 , 33 % , 8 % ) ) ;
[ data-ln-header-cell = " true " ] {
background-color : light-dark( hsla ( 175 , 12 % , 92 % , 1 ), hsla ( 177 , 19 % , 17 % , 1 ) ) ;
color : light-dark( hsla ( 177 , 19 % , 17 % , 1 ), hsla ( 175 , 12 % , 92 % , 1 ) ) ;
text-transform : capitalize ;
&[ data-ln-type =" number "] {
[ data-ln-row-detail = " true " ] {
border : 1 px solid light-dark( rgb ( 216 , 202 , 202 ), rgb ( 62 , 54 , 54 ) ) ;
background-color : light-dark( white , transparent ) ;
The applied styles are simple. Padding is added to the detail container, and a border is
applied to its direct div descendant. You can adapt this approach as needed
to fit your use case.
[ data-ln-row-detail = " true " ] {
border : 1 px solid light-dark( rgb ( 216 , 202 , 202 ), rgb ( 62 , 54 , 54 ) ) ;
background-color : light-dark( white , transparent ) ;
The cell selection rectangle consists of div elements created by LyteNyte Grid
to show the range of selected cells. This is a PRO feature available
in LyteNyte Grid PRO. You can style the selection rectangles using data
attributes, primarily data-ln-cell-selection-rect.
Cell Selection Styling import " @1771technologies/lytenyte-pro/light-dark.css " ;
import { bankDataSmall } from " @1771technologies/grid-sample-data/bank-data-smaller " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { useState } from " react " ;
export type BankData = ( typeof bankDataSmall )[ number ] ;
const columns : Grid . Column < GridSpec > [] = [
{ name : " Job " , id : " job " , width : 120 },
{ name : " Age " , id : " age " , type : " number " , width : 80 , cellRenderer : NumberCell },
{ name : " Balance " , id : " balance " , type : " number " , cellRenderer : BalanceCell },
{ name : " Education " , id : " education " },
{ name : " Marital " , id : " marital " },
{ name : " Default " , id : " default " },
{ name : " Housing " , id : " housing " },
{ name : " Loan " , id : " loan " },
{ name : " Contact " , id : " contact " },
{ name : " Day " , id : " day " , type : " number " , cellRenderer : NumberCell },
{ name : " Month " , id : " month " },
{ name : " Duration " , id : " duration " , type : " number " , cellRenderer : DurationCell },
const base : Grid . ColumnBase < GridSpec > = { width : 100 };
export default function ThemingDemo () {
const [ selections , setSelections ] = useState ([ { rowStart : 4 , rowEnd : 7 , columnStart : 2 , columnEnd : 4 } ]) ;
const ds = useClientDataSource ( { data : bankDataSmall } ) ;
< div className = " cell-styles " >
< div style = {{ height: 500 }}>
cellSelectionMode = " range "
cellSelections = { selections }
onCellSelectionChange = { setSelections }
const formatter = new Intl . NumberFormat ( " en-US " , {
maximumFractionDigits : 2 ,
minimumFractionDigits : 0 ,
export function BalanceCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
if ( typeof field === " number " ) {
if ( field < 0 ) return ` -$ ${ formatter . format ( Math . abs ( field )) }` ;
return " $ " + formatter . format ( field ) ;
return `${ field ?? " - "}` ;
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 NumberCell ({ api , row , column } : Grid . T . CellRendererParams < GridSpec >) {
const field = api . columnField ( column , row ) ;
return typeof field === " number " ? formatter . format ( field ) : `${ field ?? " - "}` ;
-webkit-user-select : none ;
background-color : light-dark( white , hsla ( 190 , 32 % , 6 % , 1 ) ) ;
color : light-dark( hsla ( 175 , 6 % , 38 % , 1 ), hsla ( 175 , 10 % , 86 % , 1 ) ) ;
border-bottom : 1 px solid light-dark( hsla ( 175 , 20 % , 95 % , 1 ), hsla ( 177 , 19 % , 17 % , 1 ) ) ;
&[ data-ln-type =" number "] {
[ data-ln-alternate = " true " ] [ data-ln-cell = " true " ] {
background-color : light-dark( hsl ( 0 , 27 % , 98 % ), hsl ( 184 , 33 % , 8 % ) ) ;
[ data-ln-header-cell = " true " ] {
background-color : light-dark( hsla ( 175 , 12 % , 92 % , 1 ), hsla ( 177 , 19 % , 17 % , 1 ) ) ;
color : light-dark( hsla ( 177 , 19 % , 17 % , 1 ), hsla ( 175 , 12 % , 92 % , 1 ) ) ;
text-transform : capitalize ;
&[ data-ln-type =" number "] {
[ data-ln-cell-selection-rect ] : not ([ data-ln-cell-selection-is-unit = " true " ]) {
background-color : rgba ( 0 , 0 , 255 , 0.08 );
&[ data-ln-cell-selection-border-top =" true "] {
border-top : 1 px solid rgba ( 0 , 0 , 255 , 1 );
& [ data-ln-cell-selection-border-bottom = " true " ] {
border-bottom : 1 px solid rgba ( 0 , 0 , 255 , 1 );
& [ data-ln-cell-selection-border-start = " true " ] {
border-inline-start : 1 px solid rgba ( 0 , 0 , 255 , 1 );
& [ data-ln-cell-selection-border-end = " true " ] {
border-inline-end : 1 px solid rgba ( 0 , 0 , 255 , 1 );
[ data-ln-cell-selection-rect ][ data-ln-cell-selection-is-unit = " true " ] {
outline : 1 px solid rgba ( 0 , 0 , 255 , 1 );
In this example, the cell selection rectangle is styled using the CSS below,
which targets the selection rectangles generated by LyteNyte Grid
when selection ranges are active. Notice how individual rectangles are selected, as
the selection area is actually a set of split divs assembled to visually form the selection rectangle.
[ data-ln-cell-selection-rect ] : not ([ data-ln-cell-selection-is-unit = " true " ]) {
background-color : rgba ( 0 , 0 , 255 , 0.08 );
&[ data-ln-cell-selection-border-top =" true "] {
border-top : 1 px solid rgba ( 0 , 0 , 255 , 1 );
& [ data-ln-cell-selection-border-bottom = " true " ] {
border-bottom : 1 px solid rgba ( 0 , 0 , 255 , 1 );
& [ data-ln-cell-selection-border-start = " true " ] {
border-inline-start : 1 px solid rgba ( 0 , 0 , 255 , 1 );
& [ data-ln-cell-selection-border-end = " true " ] {
border-inline-end : 1 px solid rgba ( 0 , 0 , 255 , 1 );
[ data-ln-cell-selection-rect ][ data-ln-cell-selection-is-unit = " true " ] {
outline : 1 px solid rgba ( 0 , 0 , 255 , 1 );
LyteNyte Grid is headless, but the default view setup is usually the recommended way to render grid cells.
Sometimes you need to apply styles directly to a cell or header, and normal CSS selectors are
insufficient (see the sticky column group labels demo for an example).
In these cases, use the styles property on the grid to apply CSS classes or
inline styles to specific grid components.
This is demonstrated in the example code below:
insetInlineStart: " var(--ln-start-offset) " ,
insetInlineStart: " var(--ln-start-offset) " ,
return < Grid.HeaderCell cell = { c } key = { c . id } />;
Both approaches are equivalent, and you do not pay a performance penalty for choosing one over the other.
You also do not need to memoize the styles property. LyteNyte Grid diffs the styles object
and notifies cells when the styles change.
For more guidance on the headless setup of LyteNyte Grid,
see the Headless Component Parts guide .
LyteNyte Grid is mostly headless and unstyled, but some functional inline
styles are necessary for layout and rendering. These styles control cell
sizing and positioning. Avoid overriding them to ensure the grid renders correctly.
Functional styles typically affect properties such as
width, height, top, left, and transform. Avoid applying
margin directly to grid elements, as it may interfere with layout calculations.
LyteNyte Grid applies its functional styles inline, which gives them high specificity.
As a result, it is difficult to unintentionally override these styles.
In most cases, conflicts will only occur if you create an extremely specific CSS selector.
This guide covers the main styling approaches for LyteNyte Grid. Since most developers
use a framework or CSS abstraction, we also provide dedicated guides for popular styling methods.