Theming
Grid Theming With Tailwind Copy Page Open additional page export options
Tailwind can be used to style LyteNyte Grid. This guide explains how to set up Tailwind and apply it to grid elements.
This guide uses Tailwind as an atomic CSS framework for styling LyteNyte Grid because it is
the most popular option. The approach described here also works with other atomic
CSS frameworks, such as UnoCSS .
We highly recommend reading the general Grid Theming guide first to
understand the styling options available in LyteNyte Grid.
This guide assumes you are using Tailwind v4. Translating
the examples back to Tailwind v3 is straightforward.
Start by setting up Tailwind. The exact steps depend on your React framework.
See Tailwind’s Installation guide for framework-specific details.
This guide assumes you have Tailwind configured with the default setup, and a CSS file that contains:
If you are not sure which framework to use, we recommend starting with
Vite . The project setup is simple and Tailwind
is supported by an official plugin.
LyteNyte Grid exposes a set of headless components that you use to build the grid view.
These components accept standard HTML and React props, including className.
As a result, styling the grid with Tailwind is as simple as applying utility classes.
The example below demonstrates this using the standard Tailwind color palette:
Styling With Tailwind 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 ;
" flex items-center border-b border-neutral-100 bg-neutral-200 px-2 text-sm capitalize text-neutral-900 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-100 " +
( cell . type === " number " ? " justify-end " : "" )
if ( row . kind === " full-width " ) return null ;
< Grid.Row row = { row } className = " group " >
{ row . cells . map (( cell ) => {
" flex items-center border-b border-neutral-200 bg-white px-2 text-sm text-neutral-800 group-data-[ln-alternate=true]:bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-200 dark:group-data-[ln-alternate=true]:bg-neutral-950 " +
( cell . type === " number " ? " justify-end tabular-nums " : "" )
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 ?? " - "}` ;
In the example, Tailwind classes are applied in the usual way.
No special configuration is required. For example, the Grid.Cell component is styled as follows:
< Grid.Cell key = { c . id } cell = { c } className = " flex items-center bg-gray-50 px-2 text-sm text-gray-800 " />
Directly applying classes to elements works well for simple styling, but it
does not scale when there is a lot of conditional styling being applied.
Tailwind utility helpers can make conditional styles easier to manage. The approach here uses three libraries:
Start by creating a tw utility function that combines clsx and tailwind-merge:
import type { ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
export function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
If you already use a Tailwind-based framework such as shadcn/ui, a similar tw function
may already exist, often named cn in shadcn/ui projects.
Next, define grid-cell variants using class-variance-authority:
const cellStyles = cva ( " flex items-center px-2 " , {
true : " justify-end tabular-nums " ,
true : " border-b border-neutral-200 bg-white text-sm text-neutral-800 group-data-[ln-alternate=true]:bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-200 dark:group-data-[ln-alternate=true]:bg-neutral-950 " ,
true : " border-b border-neutral-100 bg-neutral-200 text-sm capitalize text-neutral-900 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-100 " ,
The cellStyles function now returns the base classes and applies
variants on top for different grid-cell states.
The example below shows how to combine Tailwind, tw, and cva to provide
rich, composable grid styles:
Tailwind Styling With CVA import { cva } from " class-variance-authority " ;
import { bankDataSmall } from " @1771technologies/grid-sample-data/bank-data-smaller " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { clsx , type ClassValue } from " clsx " ;
import { twMerge } from " tailwind-merge " ;
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 function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
const cellStyles = cva ( " flex items-center px-2 " , {
true : " justify-end tabular-nums " ,
true : " border-b border-neutral-200 bg-white text-sm text-neutral-800 group-data-[ln-alternate=true]:bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-200 dark:group-data-[ln-alternate=true]:bg-neutral-950 " ,
true : " border-b border-neutral-100 bg-neutral-200 text-sm capitalize text-neutral-900 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-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 ;
className = { tw ( cellStyles ({ number: cell . type === " number " , header: true }))}
if ( row . kind === " full-width " ) return null ;
< Grid.Row row = { row } className = " group " >
{ row . cells . map (( cell ) => {
number: cell . type === " 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 ?? " - "}` ;
Notice how the CVA-generated style is applied to the grid cell:
number: cell . type === " number " ,
This pattern keeps styles consistent and makes it easy to maintain variants over a shared base.
The Grid Theming guide explains that some elements
are not directly exposed through LyteNyte Grid’s public component interface.
You must style these elements using data attributes or other selectors.
You can still target these elements with Tailwind by using more advanced selector
syntax. This section shows how to style the row-detail element and the cell selection ranges with Tailwind.
To style the row-detail element with Tailwind, target the element that has the
data-ln-row-detail attribute. You can do this without a separate
CSS file by using Tailwind’s arbitrary variant selector syntax.
Tailwind Styling Row Detail import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { companiesWithPricePerf } from " @1771technologies/grid-sample-data/companies-with-price-performance " ;
import { useMemo } from " react " ;
import { Area , AreaChart , CartesianGrid , ResponsiveContainer , XAxis , YAxis } from " recharts " ;
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 }}>
rowDetailRenderer = { rowDetailRenderer }
rowDetailExpansions = { rowDetailExpansions }
if ( cell . kind === " group " ) return null ;
" flex items-center border-b border-neutral-100 bg-neutral-200 px-2 text-sm capitalize text-neutral-900 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-100 " +
( cell . type === " number " ? " justify-end " : "" )
if ( row . kind === " full-width " ) return null ;
className = ' **:data-[ln-row-detail="true"]:p-7 group [&_[data-ln-row-detail="true"]>div]:rounded-lg [&_[data-ln-row-detail="true"]>div]:border [&_[data-ln-row-detail="true"]>div]:border-neutral-200 dark:[&_[data-ln-row-detail="true"]>div]:border-neutral-700 '
{ row . cells . map (( cell ) => {
" flex items-center border-b border-neutral-200 bg-white px-2 text-sm text-neutral-800 group-data-[ln-alternate=true]:bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-200 dark:group-data-[ln-alternate=true]:bg-neutral-950 " +
( cell . type === " number " ? " justify-end tabular-nums " : "" )
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 ?? " - "}` ;
The selector (shown below) looks complex but it is straightforward. The & symbol refers to the current
element. The [data-ln-row-detail="true"] portion selects the descendant that has the matching
data attribute. Finally, >div selects the div that is a direct descendant. If you are
unfamiliar with this syntax, see Tailwind’s guide on
arbitrary variants .
' **:data-[ln-row-detail="true"]:p-7 [&_[data-ln-row-detail="true"]>div]:rounded-lg ' ,
' [&_[data-ln-row-detail="true"]>div]:border ' ,
' [&_[data-ln-row-detail="true"]>div]:border-neutral-200 ' ,
' dark:[&_[data-ln-row-detail="true"]>div]:border-neutral-700 ' ,
This example styles the row-detail element inline. You can also move these
styles into a separate CSS file and target the same data attribute selectors there.
Tailwind compiles down to vanilla CSS, so both approaches work.
You can style cell-selection rectangles using arbitrary Tailwind selectors in a similar way.
In this case, you place the selector on the rows container rather than on each row.
The example below demonstrates this:
Cell Selection Styling With Tailwind import { bankDataSmall } from " @1771technologies/grid-sample-data/bank-data-smaller " ;
import { Grid , useClientDataSource } from " @1771technologies/lytenyte-pro " ;
import { clsx , type ClassValue } from " clsx " ;
import { useState } from " react " ;
import { twMerge } from " tailwind-merge " ;
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 };
function tw ( ... c : ClassValue [] ) {
return twMerge ( clsx ( ... c )) ;
export default function ThemingDemo () {
const [ selections , setSelections ] = useState ([ { rowStart : 4 , rowEnd : 7 , columnStart : 2 , columnEnd : 4 } ]) ;
const ds = useClientDataSource ( { data : bankDataSmall } ) ;
< div style = {{ height: 500 }} className = " select-none " >
cellSelectionMode = " range "
cellSelections = { selections }
onCellSelectionChange = { setSelections }
if ( cell . kind === " group " ) return null ;
" flex items-center border-b border-neutral-100 bg-neutral-200 px-2 text-sm capitalize text-neutral-900 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-100 " +
( cell . type === " number " ? " justify-end " : "" )
' **:not-data-[ln-cell-selection-is-unit="true"]:data-ln-cell-selection-rect:bg-blue-500/20 **:data-ln-cell-selection-rect:border-blue-500 ' ,
' **:not-data-[ln-cell-selection-is-unit="true"]:data-[ln-cell-selection-border-top=true]:border-t ' ,
' **:not-data-[ln-cell-selection-is-unit="true"]:data-[ln-cell-selection-border-bottom=true]:border-b ' ,
' **:not-data-[ln-cell-selection-is-unit="true"]:data-[ln-cell-selection-border-start=true]:border-l ' ,
' **:not-data-[ln-cell-selection-is-unit="true"]:data-[ln-cell-selection-border-end=true]:border-r ' ,
' **:data-[ln-cell-selection-is-unit="true"]:outline **:data-[ln-cell-selection-is-unit="true"]:outline-blue-500 **:data-[ln-cell-selection-is-unit="true"]:-outline-offset-1 ' ,
if ( row . kind === " full-width " ) return null ;
< Grid.Row row = { row } className = " group " >
{ row . cells . map (( cell ) => {
" flex items-center border-b border-neutral-200 bg-white px-2 text-sm text-neutral-800 group-data-[ln-alternate=true]:bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-200 dark:group-data-[ln-alternate=true]:bg-neutral-950 " +
( cell . type === " number " ? " justify-end tabular-nums " : "" )
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 ?? " - "}` ;
Here we use a descendant selector to target every cell selection rectangle.
This is a direct inline approach, but you can also move these rules
to a separate CSS file if you prefer.
' **:not-data-[ln-cell-selection-is-unit="true"]:data-ln-cell-selection-rect:bg-ln-primary-30 **:data-ln-cell-selection-rect:border-ln-primary-50 ' ,
' **:not-data-[ln-cell-selection-is-unit="true"]:data-[ln-cell-selection-border-top=true]:border-t ' ,
' **:not-data-[ln-cell-selection-is-unit="true"]:data-[ln-cell-selection-border-bottom=true]:border-b ' ,
' **:not-data-[ln-cell-selection-is-unit="true"]:data-[ln-cell-selection-border-start=true]:border-l ' ,
' **:not-data-[ln-cell-selection-is-unit="true"]:data-[ln-cell-selection-border-end=true]:border-r ' ,
' **:data-[ln-cell-selection-is-unit="true"]:outline **:data-[ln-cell-selection-is-unit="true"]:outline-ln-primary-50 **:data-[ln-cell-selection-is-unit="true"]:-outline-offset-1 ' ,
Fully theming LyteNyte Grid from scratch can require a significant amount of styling.
If you want the grid to match your design system exactly, you may choose to define every
style yourself. However, starting from a pre-built theme and then adjusting
styles with Tailwind is often more efficient.
Because Tailwind does not restrict other CSS inputs, you can combine a pre-built LyteNyte Grid
theme with Tailwind utilities. This is how many of the demos on the 1771 Technologies website
are built. The example below shows Tailwind working alongside the provided grid themes:
Pre-built Themes With Tailwind 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 >
Depending on the order of CSS imports in your application, Tailwind styles may override LyteNyte Grid
styles. This happens because the pre-built LyteNyte Grid styles are defined
in the ln-grid CSS layer.
To ensure Tailwind and LyteNyte Grid work together as expected, set the layer order in your application
as follows:
@layer base, ln-grid, components, utilities ;
For more guidance on CSS layers, see
this MDN guide .
To make styling LyteNyte Grid with Tailwind easier, the LyteNyte Grid packages export
a Tailwind-specific CSS file with useful variants and Tailwind theme values.
The full content is available on our
GitHub repository .
To use the LyteNyte Grid Tailwind variants, import the file in the same CSS file where you
set up Tailwind:
@import " @1771technologies/lytenyte-core/tw.css " ;
@import " @1771technologies/lytenyte-pro/tw.css " ;
After you import it, you can use the custom variants defined in the file. For example,
the ln-cell variant sets the cell background in the grid to the value of the
ln-bg token.
< Grid className = " ln-cell:bg-ln-bg " />
This guide covered the basics of styling LyteNyte Grid with Tailwind. For more details, see: