Row Banding
Use alternating row background to improve visual separation between rows.
Row banding, also known as zebra striping, alternates row background colors to improve
readability in dense tables. By default, LyteNyte Grid adds a data-ln-alternate="true"
attribute to every other row. Target this attribute in CSS to apply alternating styles.
Alternate Row Shading
Set rowAlternateAttr to false to disable alternating row backgrounds.
In the demo below, turn the switch off to give all rows the same background, or turn it on to restore the alternating pattern.
Striped Rows
80 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import "@1771technologies/lytenyte-pro/pill-manager.css";3import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";4import {5 ExchangeCell,6 makePerfHeaderCell,7 NetworkCell,8 PercentCell,9 PercentCellPositiveNegative,10 SwitchToggle,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";15import { useState } from "react";16
17export interface GridSpec {18 readonly data: DEXPerformanceData;19}20
21const columns: Grid.Column<GridSpec>[] = [22 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },23 { id: "network", cellRenderer: NetworkCell, width: 220, hide: true, name: "Network" },24 { id: "exchange", cellRenderer: ExchangeCell, width: 220, hide: true, name: "Exchange" },25
26 {27 id: "change24h",28 cellRenderer: PercentCellPositiveNegative,29 headerRenderer: makePerfHeaderCell("Change", "24h"),30 name: "Change % 24h",31 type: "number,",32 },33
34 {35 id: "perf1w",36 cellRenderer: PercentCellPositiveNegative,37 headerRenderer: makePerfHeaderCell("Perf %", "1w"),38 name: "Perf % 1W",39 type: "number,",40 },41 {42 id: "perf1m",43 cellRenderer: PercentCellPositiveNegative,44 headerRenderer: makePerfHeaderCell("Perf %", "1m"),45 name: "Perf % 1M",46 type: "number,",47 },48 {49 id: "perf3m",50 cellRenderer: PercentCellPositiveNegative,51 headerRenderer: makePerfHeaderCell("Perf %", "3m"),52 name: "Perf % 3M",53 type: "number,",54 },55 {56 id: "perf6m",57 cellRenderer: PercentCellPositiveNegative,58 headerRenderer: makePerfHeaderCell("Perf %", "6m"),59 name: "Perf % 6M",60 type: "number,",61 },62 {63 id: "perfYtd",64 cellRenderer: PercentCellPositiveNegative,65 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),66 name: "Perf % YTD",67 type: "number",68 },69 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },70 {71 id: "volatility1m",72 cellRenderer: PercentCell,73 headerRenderer: makePerfHeaderCell("Volatility", "1m"),74 name: "Volatility 1M",75 type: "number",76 },77];78
79const base: Grid.ColumnBase<GridSpec> = { width: 80 };80
81export default function RowDemo() {82 const ds = useClientDataSource({ data: data });83 const [alternate, setAlternate] = useState(true);84
85 return (86 <>87 <div className="border-ln-border flex w-full border-b px-2 py-2">88 <SwitchToggle89 label="Row Shading Alternation"90 checked={alternate}91 onChange={() => {92 setAlternate((prev) => !prev);93 }}94 />95 </div>96 <div97 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight"98 style={{ height: 500 }}99 >100 <Grid columns={columns} columnBase={base} rowSource={ds} rowAlternateAttr={alternate} />101 </div>102 </>103 );104}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { useId, type CSSProperties } from "react";12import { Switch } from "radix-ui";13
14export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {15 if (!api.rowIsLeaf(row) || !row.data) return null;16
17 const ticker = row.data.symbolTicker;18 const symbol = row.data.symbol;19 const image = symbols[row.data.symbolTicker];20
21 return (22 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">23 <div>24 <img25 src={image}26 alt={`Logo for symbol ${symbol}`}27 className="h-full w-full overflow-hidden rounded-full"28 />29 </div>30 <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]">31 {ticker}32 </div>33 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>34 </div>35 );36}37
38export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {39 if (!api.rowIsLeaf(row) || !row.data) return null;40
41 const name = row.data.network;42 const image = networks[name];43
44 return (45 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">46 <div>47 <img48 src={image}49 alt={`Logo for network ${name}`}50 className="h-full w-full overflow-hidden rounded-full"51 />52 </div>53 <div className="w-full overflow-hidden text-ellipsis">{name}</div>54 </div>55 );56}57
58export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {59 if (!api.rowIsLeaf(row) || !row.data) return null;60
61 const name = row.data.exchange;62 const image = exchanges[name];63
64 return (65 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">66 <div>67 <img68 src={image}69 alt={`Logo for exchange ${name}`}70 className="h-full w-full overflow-hidden rounded-full"71 />72 </div>73 <div className="w-full overflow-hidden text-ellipsis">{name}</div>74 </div>75 );76}77
78export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {79 if (!api.rowIsLeaf(row) || !row.data) return null;80
81 const field = api.columnField(column, row);82
83 if (typeof field !== "number") return "-";84
85 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";86
87 return (88 <div89 className={tw(90 "h-ful flex w-full items-center justify-end tabular-nums",91 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",92 )}93 >94 {value}95 </div>96 );97}98
99export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {100 if (!api.rowIsLeaf(row) || !row.data) return null;101
102 const field = api.columnField(column, row);103
104 if (typeof field !== "number") return "-";105
106 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";107
108 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;109}110
111export const makePerfHeaderCell = (name: string, subname: string) => {112 return (_: Grid.T.HeaderParams<GridSpec>) => {113 return (114 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">115 <div>{name}</div>116 <div className="text-ln-text-light font-mono uppercase">{subname}</div>117 </div>118 );119 };120};121
122export function SwitchToggle(props: { label: string; checked: boolean; onChange: (b: boolean) => void }) {123 const id = useId();124 return (125 <div className="flex items-center gap-2">126 <label className="text-ln-text-dark text-sm leading-none" htmlFor={id}>127 {props.label}128 </label>129 <Switch.Root130 className="bg-ln-gray-10 data-[state=checked]:bg-ln-primary-50 h-5.5 w-9.5 border-ln-border-strong relative cursor-pointer rounded-full border outline-none"131 id={id}132 checked={props.checked}133 onCheckedChange={(c) => props.onChange(c)}134 style={{ "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)" } as CSSProperties}135 >136 <Switch.Thumb className="size-4.5 block translate-x-0.5 rounded-full bg-white/95 shadow transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white" />137 </Switch.Root>138 </div>139 );140}Note
rowAlternateAttr only controls whether LyteNyte Grid applies the data-ln-alternate attribute.
To create visible row striping, target the data-ln-alternate attribute in your CSS.
Alternate Root Row Shading
You can apply alternate shading at the root row level. Passing "root" to
rowAlternateAttr makes LyteNyte Grid assign the data-ln-alternate value
based on the root row’s index rather than each individual row’s index.
When row groups are present, all children in a group inherit the
same data-ln-alternate value as their top-level parent row.
Striped Root Rows
37 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";3import { useState } from "react";4import { loanData, type LoanDataItem } from "@1771technologies/grid-sample-data/loan-data";5import {6 CountryCell,7 CustomerRating,8 DateCell,9 DurationCell,10 NameCell,11 NumberCell,12 OverdueCell,13 SwitchToggle,14} from "./components.jsx";15
16export interface GridSpec {17 readonly data: LoanDataItem;18}19
20const columns: Grid.Column<GridSpec>[] = [21 { name: "Name", id: "name", cellRenderer: NameCell, width: 110 },22 { name: "Country", id: "country", width: 150, cellRenderer: CountryCell },23 { name: "Loan Amount", id: "loanAmount", width: 120, type: "number", cellRenderer: NumberCell },24 { name: "Balance", id: "balance", type: "number", cellRenderer: NumberCell },25 { name: "Customer Rating", id: "customerRating", type: "number", width: 125, cellRenderer: CustomerRating },26 { name: "Marital", id: "marital" },27 { name: "Education", id: "education", hide: true },28 { name: "Job", id: "job", width: 120, hide: true },29 { name: "Overdue", id: "overdue", cellRenderer: OverdueCell },30 { name: "Duration", id: "duration", type: "number", cellRenderer: DurationCell },31 { name: "Date", id: "date", width: 110, cellRenderer: DateCell },32 { name: "Age", id: "age", width: 80, type: "number" },33 { name: "Contact", id: "contact" },34];35
36const base: Grid.ColumnBase<GridSpec> = { width: 100 };37
38
39const group: Grid.RowGroupColumn<GridSpec> = {40 cellRenderer: ({ row, api }) => {41 if (!api.rowIsGroup(row)) return "";42
43 return (44 <div className="flex h-full w-full items-center gap-2" style={{ paddingInlineStart: 16 * row.depth }}>45 <button46 className="size-5 cursor-pointer"47 style={{ transform: row.expanded ? "rotate(90deg)" : undefined }}48 onClick={() => api.rowGroupToggle(row)}49 >50 <CaretRight />51 </button>52
53 <div>{row.key}</div>54 </div>55 );56 },57 width: 200,58 pin: "start",59};60
61const groupFn: Grid.T.GroupFn<GridSpec["data"]> = (row) => {62 return [row.data.job, row.data.education];63};64
65export default function ClientDemo() {66 const [expansions, setExpansions] = useState<Record<string, boolean | undefined>>({67 Administration: true,68 "Blue-Collar": true,69 Management: true,70 });71
72 const ds = useClientDataSource<GridSpec>({73 data: loanData,74 group: groupFn,75 rowGroupExpansions: expansions,76 onRowGroupExpansionChange: setExpansions,77 });78
79 const [alternate, setAlternate] = useState<"root" | false>("root");80
81 return (82 <>83 <div className="border-ln-border flex w-full border-b px-2 py-2">84 <SwitchToggle85 label="Root Row Shading Alternation"86 checked={!!alternate}87 onChange={() => {88 setAlternate((prev) => (prev === "root" ? false : "root"));89 }}90 />91 </div>92
93 <div className="ln-grid ln-header:data-[ln-colid=overdue]:justify-center" style={{ height: 500 }}>94 <Grid95 rowSource={ds}96 columns={columns}97 columnBase={base}98 rowGroupColumn={group}99 rowAlternateAttr={alternate}100 />101 </div>102 </>103 );104}105
106function CaretRight() {107 return (108 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentcolor" viewBox="0 0 256 256">109 <path d="M181.66,133.66l-80,80A8,8,0,0,1,88,208V48a8,8,0,0,1,13.66-5.66l80,80A8,8,0,0,1,181.66,133.66Z"></path>110 </svg>111 );112}1import { type Grid } from "@1771technologies/lytenyte-pro";2import type { GridSpec } from "./demo.jsx";3import { twMerge } from "tailwind-merge";4import clsx, { type ClassValue } from "clsx";5import { countryFlags } from "@1771technologies/grid-sample-data/loan-data";6import { useId, useMemo, type CSSProperties } from "react";7import { format, isValid, parse } from "date-fns";8import { Switch } from "radix-ui";9
10export function tw(...c: ClassValue[]) {11 return twMerge(clsx(...c));12}13
14const formatter = new Intl.NumberFormat("en-US", {15 maximumFractionDigits: 2,16 minimumFractionDigits: 0,17});18export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {19 const field = api.columnField(column, row);20
21 if (typeof field !== "number") return "-";22
23 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);24
25 return (26 <div27 className={tw(28 "flex h-full w-full items-center justify-end tabular-nums",29 field < 0 && "text-red-600 dark:text-red-300",30 field > 0 && "text-green-600 dark:text-green-300",31 )}32 >33 {formatted}34 </div>35 );36}37
38export function NameCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {39 if (!api.rowIsLeaf(row) || !row.data) return;40
41 const url = row.data?.avatar;42
43 const name = row.data.name;44
45 return (46 <div className="flex h-full w-full items-center gap-2">47 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />48 <div className="text-ln-text-dark flex flex-col gap-0.5">49 <div>{name}</div>50 </div>51 </div>52 );53}54
55export function DurationCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {56 const field = api.columnField(column, row);57
58 return typeof field === "number" ? `${formatter.format(field)} days` : `${field ?? "-"}`;59}60
61export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {62 const field = api.columnField(column, row);63
64 const flag = countryFlags[field as keyof typeof countryFlags];65 if (!flag) return "-";66
67 return (68 <div className="flex h-full w-full items-center gap-2">69 <img className="size-4" src={flag} alt={`country flag of ${field}`} />70 <span>{String(field ?? "-")}</span>71 </div>72 );73}74
75export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {76 const field = api.columnField(column, row);77
78 if (typeof field !== "string") return "-";79
80 const dateField = parse(field as string, "yyyy-MM-dd", new Date());81
82 if (!isValid(dateField)) return "-";83
84 const niceDate = format(dateField, "yyyy MMM dd");85 return <div className="flex h-full w-full items-center tabular-nums">{niceDate}</div>;86}87
88export function OverdueCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {89 const field = api.columnField(column, row);90 if (field !== "Yes" && field !== "No") return "-";91
92 return (93 <div94 className={tw(95 "flex w-full items-center justify-center rounded-lg py-1 font-bold",96 field === "No" && "bg-green-500/10 text-green-600",97 field === "Yes" && "bg-red-500/10 text-red-400",98 )}99 >100 {field}101 </div>102 );103}104
105export function CustomerRating({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {106 const field = api.columnField(column, row);107 if (typeof field !== "number") return String(field ?? "-");108
109 return (110 <div className="flex justify-center text-yellow-300">111 <StarRating value={field} />112 </div>113 );114}115
116export default function StarRating({ value = 0 }: { value: number }) {117 const uid = useId();118
119 const max = 5;120
121 const clamped = useMemo(() => {122 const n = Number.isFinite(value) ? value : 0;123 return Math.max(0, Math.min(max, n));124 }, [value, max]);125
126 const stars = useMemo(() => {127 return Array.from({ length: max }, (_, i) => {128 const fill = Math.max(0, Math.min(1, clamped - i)); // 0..1129 return { i, fill };130 });131 }, [clamped, max]);132
133 return (134 <div className={"inline-flex items-center"} role="img">135 {stars.map(({ i, fill }) => {136 const gradId = `${uid}-star-${i}`;137 return <StarIcon key={i} fillFraction={fill} gradientId={gradId} />;138 })}139 </div>140 );141}142
143function StarIcon({ fillFraction, gradientId }: { fillFraction: number; gradientId: string }) {144 const pct = Math.round(fillFraction * 100);145
146 return (147 <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">148 <defs>149 <linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">150 <stop offset={`${pct}%`} stopColor="currentColor" />151 <stop offset={`${pct}%`} stopColor="transparent" />152 </linearGradient>153 </defs>154
155 <path156 d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"157 fill={`url(#${gradientId})`}158 />159 {/* Optional outline for crisp edges */}160 <path161 d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"162 fill="none"163 stroke="transparent"164 strokeWidth="1"165 opacity="0.35"166 />167 </svg>168 );169}170
171export function SwitchToggle(props: { label: string; checked: boolean; onChange: (b: boolean) => void }) {172 const id = useId();173 return (174 <div className="flex items-center gap-2">175 <label className="text-ln-text-dark text-sm leading-none" htmlFor={id}>176 {props.label}177 </label>178 <Switch.Root179 className="bg-ln-gray-10 data-[state=checked]:bg-ln-primary-50 h-5.5 w-9.5 border-ln-border-strong relative cursor-pointer rounded-full border outline-none"180 id={id}181 checked={props.checked}182 onCheckedChange={(c) => props.onChange(c)}183 style={{ "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)" } as CSSProperties}184 >185 <Switch.Thumb className="size-4.5 block translate-x-0.5 rounded-full bg-white/95 shadow transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white" />186 </Switch.Root>187 </div>188 );189}Next Steps
- Row Pinning: Freeze rows at the top or bottom of the viewport.
- Row Selection: Select single or multiple rows.
- Client Row Grouping: Create a hierarchical representation of your data by grouping rows.
