Client Row Pinning
Configure the client row source to pin rows to the top or bottom of the grid viewport. Pinned rows will remain visible as the user scrolls.
Info
For a high-level overview of pinned rows, see the Row Pinning guide. This guide covers row pinning specifically when using the client row source.
Pinning Rows Top
To pin rows to the top of the grid, set the topData property on the client row data source.
The demo below pins two rows to the top of the viewport. Notice that these rows remain visible
as you scroll.
Top Pinned Rows
1import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";2
77 collapsed lines
3import "@1771technologies/lytenyte-pro/light-dark.css";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 ClientDemo() {81 const ds = useClientDataSource({82 data: data.slice(2),83 topData: data.slice(0, 2),84 });85
86 return (87 <div88 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight"89 style={{ height: 500 }}90 >91 <Grid columns={columns} columnBase={base} rowSource={ds} slotShadows={ViewportShadows} />92 </div>93 );94}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};Pinning Rows Bottom
To pin rows to the bottom of the grid, set the botData property on the client row data source.
The demo below pins two rows to the bottom of the viewport. These rows remain fixed to the bottom
of the viewport even as you scroll.
Bottom Pinned Rows
1import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";2
77 collapsed lines
3import "@1771technologies/lytenyte-pro/light-dark.css";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 ClientDemo() {81 const ds = useClientDataSource({82 data: data.slice(2),83 botData: data.slice(0, 2),84 });85
86 return (87 <div88 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight"89 style={{ height: 500 }}90 >91 <Grid columns={columns} columnBase={base} rowSource={ds} slotShadows={ViewportShadows} />92 </div>93 );94}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};Pinning Rows Top and Bottom
You can pin rows to both the top and bottom of the viewport by setting both topData and botData
at the same time. Since pinned rows always remain visible, you must ensure the grid’s viewport has
enough height to display them.
Top & Bottom Pinned Rows
1import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";2
77 collapsed lines
3import "@1771technologies/lytenyte-pro/light-dark.css";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 ClientDemo() {81 const ds = useClientDataSource({82 topData: data.slice(0, 2),83 data: data.slice(2, -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 Considerations
The client data source treats pinned rows as separate from the row data provided via the
data property. In particular:
- Pinned rows will never be filtered out.
- Sorting rows will not sort pinned rows.
- Pinned rows cannot be used for groups.
Next Steps
- Client Row Sorting: Sort rows in ascending or descending order with the client row source.
- Client Row Data: Learn how to manage client row data in LyteNyte Grid.
- Client Row Grouping: Create a hierarchical representation of your data by grouping rows.
- Client Row Aggregations: Aggregate row data per group to display values at the group level.
