Row Pinning
LyteNyte Grid can freeze rows at the top or bottom of the grid viewport. Pinned rows remain visible as the user scrolls.
Pinned Row Specifications
The grid’s data source defines which rows are pinned. How you specify pinned rows depends on the type of row data source in use.
The demo below uses the client row data source to demonstrate row pinning. For other data sources, see their respective guides:
Pinned Rows
79 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 SymbolCell,11} from "./components.jsx";12import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";13import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { ViewportShadows } from "@1771technologies/lytenyte-pro/components";15
16export interface GridSpec {17 readonly data: DEXPerformanceData;18}19
20const columns: Grid.Column<GridSpec>[] = [21 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },22 { id: "network", cellRenderer: NetworkCell, width: 220, name: "Network" },23 { id: "exchange", cellRenderer: ExchangeCell, width: 220, name: "Exchange" },24
25 {26 id: "change24h",27 cellRenderer: PercentCellPositiveNegative,28 headerRenderer: makePerfHeaderCell("Change", "24h"),29 name: "Change % 24h",30 type: "number,",31 },32
33 {34 id: "perf1w",35 cellRenderer: PercentCellPositiveNegative,36 headerRenderer: makePerfHeaderCell("Perf %", "1w"),37 name: "Perf % 1W",38 type: "number,",39 },40 {41 id: "perf1m",42 cellRenderer: PercentCellPositiveNegative,43 headerRenderer: makePerfHeaderCell("Perf %", "1m"),44 name: "Perf % 1M",45 type: "number,",46 },47 {48 id: "perf3m",49 cellRenderer: PercentCellPositiveNegative,50 headerRenderer: makePerfHeaderCell("Perf %", "3m"),51 name: "Perf % 3M",52 type: "number,",53 },54 {55 id: "perf6m",56 cellRenderer: PercentCellPositiveNegative,57 headerRenderer: makePerfHeaderCell("Perf %", "6m"),58 name: "Perf % 6M",59 type: "number,",60 },61 {62 id: "perfYtd",63 cellRenderer: PercentCellPositiveNegative,64 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),65 name: "Perf % YTD",66 type: "number",67 },68 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },69 {70 id: "volatility1m",71 cellRenderer: PercentCell,72 headerRenderer: makePerfHeaderCell("Volatility", "1m"),73 name: "Volatility 1M",74 type: "number",75 },76];77
78const base: Grid.ColumnBase<GridSpec> = { width: 80 };79
80export default function RowDemo() {81 const ds = useClientDataSource({82 data: data.slice(2, -2),83 topData: data.slice(0, 2),84 botData: data.slice(-2),85 });86
87 return (88 <div89 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight"90 style={{ height: 500 }}91 >92 <Grid columns={columns} columnBase={base} rowSource={ds} slotShadows={ViewportShadows} />93 </div>94 );95}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";11
12export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {13 if (!api.rowIsLeaf(row) || !row.data) return null;14
15 const ticker = row.data.symbolTicker;16 const symbol = row.data.symbol;17 const image = symbols[row.data.symbolTicker];18
19 return (20 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">21 <div>22 <img23 src={image}24 alt={`Logo for symbol ${symbol}`}25 className="h-full w-full overflow-hidden rounded-full"26 />27 </div>28 <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]">29 {ticker}30 </div>31 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>32 </div>33 );34}35
36export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {37 if (!api.rowIsLeaf(row) || !row.data) return null;38
39 const name = row.data.network;40 const image = networks[name];41
42 return (43 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">44 <div>45 <img46 src={image}47 alt={`Logo for network ${name}`}48 className="h-full w-full overflow-hidden rounded-full"49 />50 </div>51 <div className="w-full overflow-hidden text-ellipsis">{name}</div>52 </div>53 );54}55
56export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {57 if (!api.rowIsLeaf(row) || !row.data) return null;58
59 const name = row.data.exchange;60 const image = exchanges[name];61
62 return (63 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">64 <div>65 <img66 src={image}67 alt={`Logo for exchange ${name}`}68 className="h-full w-full overflow-hidden rounded-full"69 />70 </div>71 <div className="w-full overflow-hidden text-ellipsis">{name}</div>72 </div>73 );74}75
76export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {77 if (!api.rowIsLeaf(row) || !row.data) return null;78
79 const field = api.columnField(column, row);80
81 if (typeof field !== "number") return "-";82
83 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";84
85 return (86 <div87 className={tw(88 "h-ful flex w-full items-center justify-end tabular-nums",89 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",90 )}91 >92 {value}93 </div>94 );95}96
97export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {98 if (!api.rowIsLeaf(row) || !row.data) return null;99
100 const field = api.columnField(column, row);101
102 if (typeof field !== "number") return "-";103
104 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";105
106 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;107}108
109export const makePerfHeaderCell = (name: string, subname: string) => {110 return (_: Grid.T.HeaderParams<GridSpec>) => {111 return (112 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">113 <div>{name}</div>114 <div className="text-ln-text-light font-mono uppercase">{subname}</div>115 </div>116 );117 };118};Pinned Row Indices
LyteNyte Grid assigns row indices sequentially from top to bottom. When the grid includes pinned rows, the first row index belongs to the first top-pinned row, and the final index belongs to the last bottom-pinned row.
This indexing model has an important implication. When rows are pinned to the top, the row indices of scrollable rows are offset by the number of top-pinned rows. This matters when you work with rows by index, such as when retrieving a row programmatically. To access the first scrollable row by index, you must account for the number of rows pinned at the top.
The marker column demo below makes this behavior explicit:
Offset Pinned Rows
14 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import type { OrderData } from "@1771technologies/grid-sample-data/orders";3import { data } from "@1771technologies/grid-sample-data/orders";4import {5 AvatarCell,6 EmailCell,7 IdCell,8 PaymentMethodCell,9 PriceCell,10 ProductCell,11 PurchaseDateCell,12} from "./components.jsx";13import { useClientDataSource, Grid } from "@1771technologies/lytenyte-pro";14import { ViewportShadows } from "@1771technologies/lytenyte-pro/components";15
16export interface GridSpec {17 readonly data: OrderData;18}19
20const columns: Grid.Column<GridSpec>[] = [21 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },22 { id: "product", cellRenderer: ProductCell, width: 200, name: "Product" },23 { id: "price", type: "number", cellRenderer: PriceCell, width: 100, name: "Price" },24 { id: "customer", cellRenderer: AvatarCell, width: 180, name: "Customer" },25 { id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 130 },26 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },27 { id: "email", cellRenderer: EmailCell, width: 220, name: "Email" },28];29
30const marker: Grid.ColumnMarker<GridSpec> = {31 on: true,32 cellRenderer: (p) => {33 return <div className="flex h-full w-full items-center justify-center text-xs">{p.rowIndex + 1}</div>;34 },35};36
37export default function RowDemo() {38 const ds = useClientDataSource({39 data: data.slice(2, -2),40 topData: data.slice(0, 2),41 botData: data.slice(-2),42 });43
44 return (45 <div46 className="ln-grid ln-cell-marker:border-e ln-cell-marker:border-e-ln-border-strong"47 style={{ height: 500 }}48 >49 <Grid50 rowHeight={50}51 columns={columns}52 rowSource={ds}53 columnMarker={marker}54 slotShadows={ViewportShadows}55 />56 </div>57 );58}1import { format } from "date-fns";2import { type JSX, type ReactNode } from "react";3import type { Grid } from "@1771technologies/lytenyte-pro";4import type { GridSpec } from "./demo.jsx";5
6export function ProductCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {7 if (!api.rowIsLeaf(row) || !row.data) return;8
9 const url = row.data?.productThumbnail;10 const title = row.data.product;11 const desc = row.data.productDescription;12
13 return (14 <div className="flex h-full w-full items-center gap-2">15 <img className="border-ln-border-strong h-7 w-7 rounded-lg border" src={url} alt={title + desc} />16 <div className="text-ln-text-dark flex flex-col gap-0.5">17 <div className="font-semibold">{title}</div>18 <div className="text-ln-text-light text-xs">{desc}</div>19 </div>20 </div>21 );22}23
24export function AvatarCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {25 if (!api.rowIsLeaf(row) || !row.data) return;26
27 const url = row.data?.customerAvatar;28
29 const name = row.data.customer;30
31 return (32 <div className="flex h-full w-full items-center gap-2">33 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />34 <div className="text-ln-text-dark flex flex-col gap-0.5">35 <div>{name}</div>36 </div>37 </div>38 );39}40
41const formatter = new Intl.NumberFormat("en-Us", {42 minimumFractionDigits: 2,43 maximumFractionDigits: 2,44});45export function PriceCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {46 if (!api.rowIsLeaf(row) || !row.data) return;47
48 const price = formatter.format(row.data.price);49 const [dollars, cents] = price.split(".");50
51 return (52 <div className="flex h-full w-full items-center justify-end">53 <div className="flex items-baseline tabular-nums">54 <span className="text-ln-text font-semibold">${dollars}</span>.55 <span className="relative text-xs">{cents}</span>56 </div>57 </div>58 );59}60
61export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {62 if (!api.rowIsLeaf(row) || !row.data) return;63
64 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");65
66 return <div className="flex h-full w-full items-center">{formattedDate}</div>;67}68export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {69 if (!api.rowIsLeaf(row) || !row.data) return;70
71 return <div className="text-xs tabular-nums">{row.data.id}</div>;72}73
74export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {75 if (!api.rowIsLeaf(row) || !row.data) return;76
77 const cardNumber = row.data.cardNumber;78 const provider = row.data.paymentMethod;79
80 let Logo: ReactNode = null;81 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;82 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;83
84 return (85 <div className="flex h-full w-full items-center gap-2">86 <div className="flex w-7 items-center justify-center">{Logo}</div>87 <div className="flex items-center gap-px">88 <div className="bg-ln-gray-40 size-2 rounded-full"></div>89 <div className="bg-ln-gray-40 size-2 rounded-full"></div>90 <div className="bg-ln-gray-40 size-2 rounded-full"></div>91 <div className="bg-ln-gray-40 size-2 rounded-full"></div>92 </div>93 <div className="tabular-nums">{cardNumber}</div>94 </div>95 );96}97
98export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return;100
101 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;102}103
104const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (105 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>106 <path107 fill="#1434cb"108 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"109 />110 </svg>111);112
113const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (114 <svg115 xmlns="http://www.w3.org/2000/svg"116 width={2500}117 height={1524}118 viewBox="55.2 38.3 464.5 287.8"119 {...props}120 >121 <path122 fill="#f79f1a"123 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"124 />125 <path126 fill="#ea001b"127 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"128 />129 <path130 fill="#ff5f01"131 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"132 />133 </svg>134);In this example, the first scrollable row displays an index of 3 because two rows are pinned to the top.
The bottom-pinned rows display indices 17 and 18, reflecting their position after both the
top-pinned and scrollable rows.
Next Steps
- Row Full Width: Create rows that span the full width of the viewport.
- Row Dragging: Drag and drop rows within or between LyteNyte grids, or into external drop zones and apps.
- Row Spanning: Span cells across multiple rows.
