Row Selection
Select individual or multiple rows using LyteNyte Grid's row selection system. Easily implement checkbox-based selection with support for bulk operations.
Enabling Row Selection
The rowSelectionMode property on the LyteNyte Grid state object configures which
row selection behavior the grid should use. It accepts one of the following values:
"none": Disables all built-in row selection behavior and prevents user interactions from selecting rows. Developers may still select rows programmatically."single": Allows the user to select a single row. Selecting a new row clears any existing selection."multiple": Allows multiple rows to be selected, including additive selection behavior.
In addition to rowSelectionMode, the rowSelectionActivator property controls
which user action selects a row. It accepts one of the following values:
"none": Disables all row-based selection interactions. Developers must provide a custom selection mechanism, such as selection checkboxes."single-click": Selects a row when the user clicks it."double-click": Selects a row when the user double-clicks it.
The demo below shows single-row selection configured to select rows on a single click. Other configurations can be used to support multiple selection modes or checkbox-based selection, which are covered in later sections.
Enabling Row Selection
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId } from "react";import {ExchangeCell,makePerfHeaderCell,NetworkCell,PercentCell,PercentCellPositiveNegative,SymbolCell,tw,} from "./components";import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";const columns: Column<DEXPerformanceData>[] = [{id: "symbol",cellRenderer: SymbolCell,width: 220,name: "Symbol",},{id: "network",cellRenderer: NetworkCell,width: 220,name: "Network",},{id: "exchange",cellRenderer: ExchangeCell,width: 220,name: "Exchange",},{id: "change24h",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Change", "24h"),name: "Change % 24h",type: "number,",},{id: "perf1w",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "1w"),name: "Perf % 1W",type: "number,",},{id: "perf1m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "1m"),name: "Perf % 1M",type: "number,",},{id: "perf3m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "3m"),name: "Perf % 3M",type: "number,",},{id: "perf6m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "6m"),name: "Perf % 6M",type: "number,",},{id: "perfYtd",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "YTD"),name: "Perf % YTD",type: "number",},{ id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },{id: "volatility1m",cellRenderer: PercentCell,headerRenderer: makePerfHeaderCell("Volatility", "1m"),name: "Volatility 1M",type: "number",},];export default function RowSelection() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 80 },rowSelectionMode: "single",rowSelectionActivator: "single-click",});const view = grid.view.useValue();return (<div><div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group")return (<Grid.HeaderGroupCellcell={c}key={c.idOccurrence}className="text-xs! flex items-center justify-center px-2"/>);return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("text-ln-gray-60! dark:text-ln-gray-70! flex h-full w-full items-center text-nowrap px-2 text-xs capitalize",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className="text-xs! flex h-full w-full items-center px-2"/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div></div>);}
import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";import {exchanges,networks,symbols,} from "@1771technologies/grid-sample-data/dex-pairs-performance";export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function SymbolCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const ticker = row.data.symbolTicker;const symbol = row.data.symbol;const image = symbols[row.data.symbolTicker];return (<div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for symbol ${symbol}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="bg-ln-gray-20 text-ln-gray-100 flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">{ticker}</div><div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div></div>);}export function NetworkCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const name = row.data.network;const image = networks[name];return (<div className="grid grid-cols-[20px_1fr] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for network ${name}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="w-full overflow-hidden text-ellipsis">{name}</div></div>);}export function ExchangeCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const name = row.data.exchange;const image = exchanges[name];return (<div className="grid grid-cols-[20px_1fr] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for exchange ${name}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="w-full overflow-hidden text-ellipsis">{name}</div></div>);}export function PercentCellPositiveNegative({grid: { api },column,row,}: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const field = api.columnField(column, row);if (typeof field !== "number") return "-";const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";return (<divclassName={tw("h-ful flex w-full items-center justify-end tabular-nums",field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",)}>{value}</div>);}export function PercentCell({grid: { api },column,row,}: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const field = api.columnField(column, row);if (typeof field !== "number") return "-";const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;}export const makePerfHeaderCell = (name: string, subname: string) => {return (_: HeaderCellRendererParams<DEXPerformanceData>) => {return (<div className="flex h-full w-full flex-col items-end justify-center"><div>{name}</div><div className="font-mono uppercase">{subname}</div></div>);};};
Multiple Row Selection
Set rowSelectionMode to "multiple" to allow users to select more than one row.
Clicking a row toggles its selection state. Holding Shift while clicking
another row selects the range between the two rows.
The demo below demonstrates these interactions.
Multiple Row Selection
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId } from "react";import {ExchangeCell,makePerfHeaderCell,NetworkCell,PercentCell,PercentCellPositiveNegative,SymbolCell,tw,} from "./components";import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";const columns: Column<DEXPerformanceData>[] = [{id: "symbol",cellRenderer: SymbolCell,width: 220,name: "Symbol",},{id: "network",cellRenderer: NetworkCell,width: 220,name: "Network",},{id: "exchange",cellRenderer: ExchangeCell,width: 220,name: "Exchange",},{id: "change24h",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Change", "24h"),name: "Change % 24h",type: "number,",},{id: "perf1w",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "1w"),name: "Perf % 1W",type: "number,",},{id: "perf1m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "1m"),name: "Perf % 1M",type: "number,",},{id: "perf3m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "3m"),name: "Perf % 3M",type: "number,",},{id: "perf6m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "6m"),name: "Perf % 6M",type: "number,",},{id: "perfYtd",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "YTD"),name: "Perf % YTD",type: "number",},{ id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },{id: "volatility1m",cellRenderer: PercentCell,headerRenderer: makePerfHeaderCell("Volatility", "1m"),name: "Volatility 1M",type: "number",},];export default function RowSelection() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 80 },rowSelectionMode: "multiple",rowSelectionActivator: "single-click",});const view = grid.view.useValue();return (<div><div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group")return (<Grid.HeaderGroupCellcell={c}key={c.idOccurrence}className="text-xs! flex items-center justify-center px-2"/>);return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("text-ln-gray-60! dark:text-ln-gray-70! flex h-full w-full items-center text-nowrap px-2 text-xs capitalize",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className="text-xs! flex h-full w-full items-center px-2"/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div></div>);}
import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";import {exchanges,networks,symbols,} from "@1771technologies/grid-sample-data/dex-pairs-performance";export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function SymbolCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const ticker = row.data.symbolTicker;const symbol = row.data.symbol;const image = symbols[row.data.symbolTicker];return (<div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for symbol ${symbol}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="bg-ln-gray-20 text-ln-gray-100 flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">{ticker}</div><div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div></div>);}export function NetworkCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const name = row.data.network;const image = networks[name];return (<div className="grid grid-cols-[20px_1fr] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for network ${name}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="w-full overflow-hidden text-ellipsis">{name}</div></div>);}export function ExchangeCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const name = row.data.exchange;const image = exchanges[name];return (<div className="grid grid-cols-[20px_1fr] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for exchange ${name}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="w-full overflow-hidden text-ellipsis">{name}</div></div>);}export function PercentCellPositiveNegative({grid: { api },column,row,}: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const field = api.columnField(column, row);if (typeof field !== "number") return "-";const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";return (<divclassName={tw("h-ful flex w-full items-center justify-end tabular-nums",field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",)}>{value}</div>);}export function PercentCell({grid: { api },column,row,}: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const field = api.columnField(column, row);if (typeof field !== "number") return "-";const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;}export const makePerfHeaderCell = (name: string, subname: string) => {return (_: HeaderCellRendererParams<DEXPerformanceData>) => {return (<div className="flex h-full w-full flex-col items-end justify-center"><div>{name}</div><div className="font-mono uppercase">{subname}</div></div>);};};
The following configuration enables multiple row selection using single-click activation:
const grid = Grid.useLyteNyte({// Other grid propsrowSelectionMode: "multiple",rowSelectionActivator: "single-click",});
Checkbox Selection
Selecting rows by clicking anywhere on the row can conflict with other grid interactions or diverge from common UX patterns. A common alternative is checkbox-based row selection.
LyteNyte Grid provides the api.rowHandleSelect helper to simplify checkbox selection logic,
including support for shift-based range selection.
Checkbox Row Selection
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId } from "react";import type { OrderData } from "@1771technologies/grid-sample-data/orders";import { data } from "@1771technologies/grid-sample-data/orders";import {AvatarCell,EmailCell,IdCell,MarkerCell,MarkerHeader,PaymentMethodCell,PriceCell,ProductCell,PurchaseDateCell,tw,} from "./components";const columns: Column<OrderData>[] = [{ id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },{ id: "product", cellRenderer: ProductCell, width: 200, type: "string" },{ id: "price", type: "number", cellRenderer: PriceCell, width: 100 },{ id: "customer", cellRenderer: AvatarCell, width: 180, type: "string" },{ id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 120 },{ id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },{ id: "email", cellRenderer: EmailCell, width: 220, type: "string" },];export default function RowSelection() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnMarkerEnabled: true,columnMarker: {cellRenderer: MarkerCell,headerRenderer: MarkerHeader,},rowSelectionMode: "multiple",rowSelectionActivator: "none",rowHeight: 50,});const view = grid.view.useValue();return (<div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("flex h-full w-full items-center text-nowrap text-sm capitalize",!c.id.startsWith("lytenyte") && "px-3",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className={tw("flex h-full w-full items-center text-sm",!c.column.id.startsWith("lytenyte") && "px-3",)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import { Checkbox as C } from "radix-ui";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import { format } from "date-fns";import { type JSX, type ReactNode } from "react";import type { OrderData } from "@1771technologies/grid-sample-data/orders";import { CheckIcon, MinusIcon } from "@radix-ui/react-icons";export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function ProductCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const url = row.data?.productThumbnail;const title = row.data.product;const desc = row.data.productDescription;return (<div className="flex h-full w-full items-center gap-2"><img className="border-ln-gray-50 h-7 w-7 rounded-lg border" src={url} alt={title + desc} /><div className="text-ln-gray-90 flex flex-col gap-0.5"><div className="font-semibold">{title}</div><div className="text-ln-gray-70 text-xs">{desc}</div></div></div>);}export function AvatarCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const url = row.data?.customerAvatar;const name = row.data.customer;return (<div className="flex h-full w-full items-center gap-2"><img className="border-ln-gray-50 h-7 w-7 rounded-full border" src={url} alt={name} /><div className="text-ln-gray-90 flex flex-col gap-0.5"><div>{name}</div></div></div>);}const formatter = new Intl.NumberFormat("en-Us", {minimumFractionDigits: 2,maximumFractionDigits: 2,});export function PriceCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const price = formatter.format(row.data.price);const [dollars, cents] = price.split(".");return (<div className="flex h-full w-full items-center justify-end"><div className="flex items-baseline tabular-nums"><span className="text-ln-gray-80 font-semibold">${dollars}</span>.<span className="relative text-xs">{cents}</span></div></div>);}export function PurchaseDateCell({ grid: { api }, row }: CellRendererParams<OrderData>) {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({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;return <div className="text-xs tabular-nums">{row.data.id}</div>;}export function PaymentMethodCell({ grid: { api }, row }: CellRendererParams<OrderData>) {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" />;return (<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"><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><div className="tabular-nums">{cardNumber}</div></div>);}export function EmailCell({ grid: { api }, row }: CellRendererParams<OrderData>) {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"]) => (<svgxmlns="http://www.w3.org/2000/svg"width={2500}height={812}viewBox="0.5 0.5 999 323.684"{...props}><pathfill="#1434cb"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"/></svg>);const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (<svgxmlns="http://www.w3.org/2000/svg"width={2500}height={1524}viewBox="55.2 38.3 464.5 287.8"{...props}><pathfill="#f79f1a"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"/><pathfill="#ea001b"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"/><pathfill="#ff5f01"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"/></svg>);export function MarkerHeader({ grid }: HeaderCellRendererParams<OrderData>) {const allSelected = grid.state.rowDataSource.get().rowAreAllSelected();const selected = grid.state.rowSelectedIds.useValue();return (<div className="flex h-full w-full items-center justify-center"><GridCheckboxchecked={allSelected || selected.size > 0}indeterminate={!allSelected && selected.size > 0}onClick={(ev) => {ev.preventDefault();grid.api.rowSelectAll({ deselect: allSelected });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowSelectAll({ deselect: allSelected });}}/></div>);}export function MarkerCell({ grid, rowSelected }: CellRendererParams<OrderData>) {return (<div className="flex h-full w-full items-center justify-center"><GridCheckboxchecked={rowSelected}onClick={(ev) => {ev.stopPropagation();grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}/></div>);}export function GridCheckbox({children,indeterminate,...props}: C.CheckboxProps & { indeterminate?: boolean }) {return (<label className="text-md text-light flex items-center gap-2"><C.Root{...props}type="button"className={tw("bg-ln-gray-02 rounded border-transparent","shadow-[0_1.5px_2px_0_rgba(18,46,88,0.08),0_0_0_1px_var(--lng1771-gray-40)]","data-[state=checked]:bg-ln-primary-50 data-[state=checked]:shadow-[0_1.5px_2px_0_rgba(18,46,88,0.08),0_0_0_1px_var(--lng1771-primary-50)]","h-4 w-4",props.className,)}><C.CheckboxIndicator className={tw("flex items-center justify-center")}>{!indeterminate && <CheckIcon className="text-white dark:text-black" />}{indeterminate && <MinusIcon className="text-white dark:text-black" />}</C.CheckboxIndicator></C.Root>{children}</label>);}
The demo uses api.rowHandleSelect to handle checkbox interactions:
<GridCheckboxonClick={(ev) => {ev.stopPropagation();grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}/>
Selecting All Rows
The checkbox demo also supports selecting all rows using a header checkbox.
This behavior uses the grid.api.rowSelectAll method.
You can call this method to select or deselect all rows currently known to the grid.
Group Row Selection
Row groups form hierarchical relationships with their child rows, but each group row maintains its own selection state. By default, selecting a group row does not affect the selection state of its children.
You can change this behavior using the rowSelectChildren property on the grid state.
In the example below, selecting a group row also selects all of its child rows.
Row Selection Group Selection
"use client";import "./main.css";import "@1771technologies/lytenyte-pro/grid.css";import { useClientRowDataSource, Grid } from "@1771technologies/lytenyte-pro";import type { Column } from "@1771technologies/lytenyte-pro/types";import { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";import { useId } from "react";import { tw } from "./ui";import { BalanceCell, DurationCell, MarkerCell, MarkerHeader, NumberCell } from "./components";import { ChevronDownIcon, ChevronRightIcon } from "@1771technologies/lytenyte-pro/icons";type BankData = (typeof bankDataSmall)[number];const columns: Column<BankData>[] = [{ id: "job", width: 120, hide: true },{ id: "age", type: "number", width: 80, cellRenderer: NumberCell },{ id: "balance", type: "number", cellRenderer: BalanceCell },{ id: "education", hide: true },{ id: "marital" },{ id: "default" },{ id: "housing" },{ id: "loan" },{ id: "contact" },{ id: "day", type: "number", cellRenderer: NumberCell },{ id: "month" },{ id: "duration", type: "number", cellRenderer: DurationCell },{ id: "poutcome", name: "P Outcome" },{ id: "y" },];export default function GridTheming() {const ds = useClientRowDataSource({ data: bankDataSmall });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,rowGroupModel: ["job", "education"],columns,columnBase: { width: 100 },rowGroupExpansions: {"+.+Unemployed": true,"+.+Unemployed/Primary": true,},rowSelectedIds: new Set(["+.+Unemployed/Primary", "0", "99", "108"]),rowSelectionMode: "multiple",rowSelectionActivator: "single-click",rowSelectChildren: true,columnMarkerEnabled: true,columnMarker: {cellRenderer: MarkerCell,headerRenderer: MarkerHeader,},rowGroupColumn: {width: 200,cellRenderer: ({ grid, row, column }) => {if (!grid.api.rowIsGroup(row)) return null;const field = grid.api.columnField(column, row);const isExpanded = grid.api.rowGroupIsExpanded(row);return (<divclassName="flex h-full w-full items-center gap-2"style={{ paddingLeft: row.depth * 16 }}><buttonclassName="flex items-center justify-center"onClick={(e) => {e.stopPropagation();grid.api.rowGroupToggle(row);}}>{!isExpanded && <ChevronRightIcon />}{isExpanded && <ChevronDownIcon />}</button><div>{`${field}`}</div></div>);},},});const view = grid.view.useValue();return (<div className="lng-grid" style={{ display: "flex", flexDirection: "column" }}><div style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("flex items-center px-2 text-sm capitalize",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className={tw("flex items-center px-2 text-sm",c.column.type === "number" && "justify-end tabular-nums",)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div></div>);}
import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import { Checkbox as C } from "radix-ui";import type { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";import { tw } from "./ui";import { CheckIcon, MinusIcon } from "@radix-ui/react-icons";export type BankData = (typeof bankDataSmall)[number];const formatter = new Intl.NumberFormat("en-US", {maximumFractionDigits: 2,minimumFractionDigits: 0,});export function BalanceCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.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({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? `${formatter.format(field)} days` : `${field ?? ""}`;}export function NumberCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? formatter.format(field) : `${field ?? ""}`;}export function MarkerHeader({ grid }: HeaderCellRendererParams<BankData>) {const allSelected = grid.state.rowDataSource.get().rowAreAllSelected();const selected = grid.state.rowSelectedIds.useValue();return (<div className="flex h-full w-full items-center justify-center"><GridCheckboxchecked={allSelected || selected.size > 0}indeterminate={!allSelected && selected.size > 0}onClick={(ev) => {ev.preventDefault();grid.api.rowSelectAll({ deselect: allSelected });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowSelectAll({ deselect: allSelected });}}/></div>);}export function MarkerCell({ grid, rowSelected, rowIndeterminate }: CellRendererParams<BankData>) {return (<div className="flex h-full w-full items-center justify-center"><GridCheckboxchecked={rowSelected || rowIndeterminate}indeterminate={rowIndeterminate}onClick={(ev) => {ev.stopPropagation();grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}/></div>);}export function GridCheckbox({children,indeterminate,...props}: C.CheckboxProps & { indeterminate?: boolean }) {return (<label className="text-md text-light flex items-center gap-2"><C.Root{...props}type="button"className={tw("bg-ln-gray-02 rounded border-transparent","shadow-[0_1.5px_2px_0_rgba(18,46,88,0.08),0_0_0_1px_var(--lng1771-gray-40)]","data-[state=checked]:bg-ln-primary-50 data-[state=checked]:shadow-[0_1.5px_2px_0_rgba(18,46,88,0.08),0_0_0_1px_var(--lng1771-primary-50)]","h-4 w-4",props.className,)}><C.CheckboxIndicator className={tw("flex items-center justify-center")}>{!indeterminate && <CheckIcon className="text-white dark:text-black" />}{indeterminate && <MinusIcon className="text-white dark:text-black" />}</C.CheckboxIndicator></C.Root>{children}</label>);}
import { ToggleGroup as TG } from "radix-ui";import type { ClassValue } from "clsx";import clsx 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]) {return (<TG.Root{...props}className={tw("bg-ln-gray-20 flex items-center gap-2 rounded-xl px-2 py-1", props.className)}></TG.Root>);}export function ToggleItem(props: Parameters<typeof TG.Item>[0]) {return (<TG.Item{...props}className={tw("text-ln-gray-70 flex items-center justify-center px-2 py-1 text-xs font-bold outline-none focus:outline-none","data-[state=on]:text-ln-gray-90 data-[state=on]:bg-linear-to-b from-ln-gray-02 to-ln-gray-05 data-[state=on]:rounded-md",props.className,)}></TG.Item>);}export function ThemePicker({ theme, setTheme }: { theme: string; setTheme: (s: string) => void }) {return (<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><ToggleGrouptype="single"value={theme}className={tw("flex flex-wrap")}onValueChange={(c) => {if (!c) return;setTheme(c);}}><ToggleItem value="light">Light</ToggleItem><ToggleItem value="dark">Dark</ToggleItem><ToggleItem value="lng1771-teal">LyteNyte Teal</ToggleItem><ToggleItem value="lng1771-term256">Term 256</ToggleItem><ToggleItem value="lng1771-shadcn-dark dark">Shadcn Dark</ToggleItem><ToggleItem value="lng1771-shadcn-light light">Shadcn Light</ToggleItem><ToggleItem value="lng1771-cotton-candy">Cotton Candy</ToggleItem></ToggleGroup></div>);}
Row Selection State
The rowSelectionIds property controls which rows are selected.
It is a set of string values, where each value represents a row ID.
The set may contain IDs that do not yet correspond to existing rows. This allows you to preselect rows before they are created or loaded. The demo below shows how to initialize the grid with a predefined selection state.
Row Selection State
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId } from "react";import type { OrderData } from "@1771technologies/grid-sample-data/orders";import { data } from "@1771technologies/grid-sample-data/orders";import {AvatarCell,EmailCell,IdCell,MarkerCell,MarkerHeader,PaymentMethodCell,PriceCell,ProductCell,PurchaseDateCell,tw,} from "./components";const columns: Column<OrderData>[] = [{ id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },{ id: "product", cellRenderer: ProductCell, width: 200, type: "string" },{ id: "price", type: "number", cellRenderer: PriceCell, width: 100 },{ id: "customer", cellRenderer: AvatarCell, width: 180, type: "string" },{ id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 120 },{ id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },{ id: "email", cellRenderer: EmailCell, width: 220, type: "string" },];export default function RowSelection() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,rowSelectedIds: new Set(["0", "2"]),columnMarkerEnabled: true,columnMarker: {cellRenderer: MarkerCell,headerRenderer: MarkerHeader,},rowSelectionMode: "multiple",rowSelectionActivator: "none",rowHeight: 50,});const view = grid.view.useValue();return (<div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("flex h-full w-full items-center text-nowrap text-sm capitalize",!c.id.startsWith("lytenyte") && "px-3",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className={tw("flex h-full w-full items-center text-sm",!c.column.id.startsWith("lytenyte") && "px-3",)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import { Checkbox as C } from "radix-ui";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import { format } from "date-fns";import { type JSX, type ReactNode } from "react";import type { OrderData } from "@1771technologies/grid-sample-data/orders";import { CheckIcon, MinusIcon } from "@radix-ui/react-icons";export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function ProductCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const url = row.data?.productThumbnail;const title = row.data.product;const desc = row.data.productDescription;return (<div className="flex h-full w-full items-center gap-2"><img className="border-ln-gray-50 h-7 w-7 rounded-lg border" src={url} alt={title + desc} /><div className="text-ln-gray-90 flex flex-col gap-0.5"><div className="font-semibold">{title}</div><div className="text-ln-gray-70 text-xs">{desc}</div></div></div>);}export function AvatarCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const url = row.data?.customerAvatar;const name = row.data.customer;return (<div className="flex h-full w-full items-center gap-2"><img className="border-ln-gray-50 h-7 w-7 rounded-full border" src={url} alt={name} /><div className="text-ln-gray-90 flex flex-col gap-0.5"><div>{name}</div></div></div>);}const formatter = new Intl.NumberFormat("en-Us", {minimumFractionDigits: 2,maximumFractionDigits: 2,});export function PriceCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const price = formatter.format(row.data.price);const [dollars, cents] = price.split(".");return (<div className="flex h-full w-full items-center justify-end"><div className="flex items-baseline tabular-nums"><span className="text-ln-gray-80 font-semibold">${dollars}</span>.<span className="relative text-xs">{cents}</span></div></div>);}export function PurchaseDateCell({ grid: { api }, row }: CellRendererParams<OrderData>) {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({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;return <div className="text-xs tabular-nums">{row.data.id}</div>;}export function PaymentMethodCell({ grid: { api }, row }: CellRendererParams<OrderData>) {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" />;return (<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"><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><div className="tabular-nums">{cardNumber}</div></div>);}export function EmailCell({ grid: { api }, row }: CellRendererParams<OrderData>) {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"]) => (<svgxmlns="http://www.w3.org/2000/svg"width={2500}height={812}viewBox="0.5 0.5 999 323.684"{...props}><pathfill="#1434cb"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"/></svg>);const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (<svgxmlns="http://www.w3.org/2000/svg"width={2500}height={1524}viewBox="55.2 38.3 464.5 287.8"{...props}><pathfill="#f79f1a"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"/><pathfill="#ea001b"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"/><pathfill="#ff5f01"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"/></svg>);export function MarkerHeader({ grid }: HeaderCellRendererParams<OrderData>) {const allSelected = grid.state.rowDataSource.get().rowAreAllSelected();const selected = grid.state.rowSelectedIds.useValue();return (<div className="flex h-full w-full items-center justify-center"><GridCheckboxchecked={allSelected || selected.size > 0}indeterminate={!allSelected && selected.size > 0}onClick={(ev) => {ev.preventDefault();grid.api.rowSelectAll({ deselect: allSelected });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowSelectAll({ deselect: allSelected });}}/></div>);}export function MarkerCell({ grid, rowSelected }: CellRendererParams<OrderData>) {return (<div className="flex h-full w-full items-center justify-center"><GridCheckboxchecked={rowSelected}onClick={(ev) => {ev.stopPropagation();grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}/></div>);}export function GridCheckbox({children,indeterminate,...props}: C.CheckboxProps & { indeterminate?: boolean }) {return (<label className="text-md text-light flex items-center gap-2"><C.Root{...props}type="button"className={tw("bg-ln-gray-02 rounded border-transparent","shadow-[0_1.5px_2px_0_rgba(18,46,88,0.08),0_0_0_1px_var(--lng1771-gray-40)]","data-[state=checked]:bg-ln-primary-50 data-[state=checked]:shadow-[0_1.5px_2px_0_rgba(18,46,88,0.08),0_0_0_1px_var(--lng1771-primary-50)]","h-4 w-4",props.className,)}><C.CheckboxIndicator className={tw("flex items-center justify-center")}>{!indeterminate && <CheckIcon className="text-white dark:text-black" />}{indeterminate && <MinusIcon className="text-white dark:text-black" />}</C.CheckboxIndicator></C.Root>{children}</label>);}
Preventing Row Selection
When a row selection begins, LyteNyte Grid fires the rowSelectBegin event.
You can use this event to prevent specific rows from being selected.
In the demo below, the grid prevents selection of rows with odd-numbered indices.
Prevent Row Selection
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId } from "react";import type { OrderData } from "@1771technologies/grid-sample-data/orders";import { data } from "@1771technologies/grid-sample-data/orders";import {AvatarCell,EmailCell,IdCell,MarkerCell,MarkerHeader,PaymentMethodCell,PriceCell,ProductCell,PurchaseDateCell,tw,} from "./components";const columns: Column<OrderData>[] = [{ id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },{ id: "product", cellRenderer: ProductCell, width: 200, type: "string" },{ id: "price", type: "number", cellRenderer: PriceCell, width: 100 },{ id: "customer", cellRenderer: AvatarCell, width: 180, type: "string" },{ id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 120 },{ id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },{ id: "email", cellRenderer: EmailCell, width: 220, type: "string" },];export default function RowSelection() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnMarkerEnabled: true,columnMarker: {cellRenderer: MarkerCell,headerRenderer: MarkerHeader,},rowSelectionMode: "multiple",rowHeight: 50,});const view = grid.view.useValue();return (<div className="lng-grid" style={{ height: 500 }}><Grid.Rootgrid={grid}onRowSelectBegin={({ preventDefault, selected }) => {if (Number.parseInt(selected) % 2) {preventDefault();alert(`Selection of the row at index ${Number.parseInt(selected)} has been prevent.`);}}}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("flex h-full w-full items-center text-nowrap text-sm capitalize",!c.id.startsWith("lytenyte") && "px-3",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className={tw("flex h-full w-full items-center text-sm",!c.column.id.startsWith("lytenyte") && "px-3",)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import { Checkbox as C } from "radix-ui";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import { format } from "date-fns";import { type JSX, type ReactNode } from "react";import type { OrderData } from "@1771technologies/grid-sample-data/orders";import { CheckIcon, MinusIcon } from "@radix-ui/react-icons";export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function ProductCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const url = row.data?.productThumbnail;const title = row.data.product;const desc = row.data.productDescription;return (<div className="flex h-full w-full items-center gap-2"><img className="border-ln-gray-50 h-7 w-7 rounded-lg border" src={url} alt={title + desc} /><div className="text-ln-gray-90 flex flex-col gap-0.5"><div className="font-semibold">{title}</div><div className="text-ln-gray-70 text-xs">{desc}</div></div></div>);}export function AvatarCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const url = row.data?.customerAvatar;const name = row.data.customer;return (<div className="flex h-full w-full items-center gap-2"><img className="border-ln-gray-50 h-7 w-7 rounded-full border" src={url} alt={name} /><div className="text-ln-gray-90 flex flex-col gap-0.5"><div>{name}</div></div></div>);}const formatter = new Intl.NumberFormat("en-Us", {minimumFractionDigits: 2,maximumFractionDigits: 2,});export function PriceCell({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;const price = formatter.format(row.data.price);const [dollars, cents] = price.split(".");return (<div className="flex h-full w-full items-center justify-end"><div className="flex items-baseline tabular-nums"><span className="text-ln-gray-80 font-semibold">${dollars}</span>.<span className="relative text-xs">{cents}</span></div></div>);}export function PurchaseDateCell({ grid: { api }, row }: CellRendererParams<OrderData>) {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({ grid: { api }, row }: CellRendererParams<OrderData>) {if (!api.rowIsLeaf(row) || !row.data) return;return <div className="text-xs tabular-nums">{row.data.id}</div>;}export function PaymentMethodCell({ grid: { api }, row }: CellRendererParams<OrderData>) {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" />;return (<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"><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><div className="tabular-nums">{cardNumber}</div></div>);}export function EmailCell({ grid: { api }, row }: CellRendererParams<OrderData>) {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"]) => (<svgxmlns="http://www.w3.org/2000/svg"width={2500}height={812}viewBox="0.5 0.5 999 323.684"{...props}><pathfill="#1434cb"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"/></svg>);const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (<svgxmlns="http://www.w3.org/2000/svg"width={2500}height={1524}viewBox="55.2 38.3 464.5 287.8"{...props}><pathfill="#f79f1a"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"/><pathfill="#ea001b"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"/><pathfill="#ff5f01"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"/></svg>);export function MarkerHeader({ grid }: HeaderCellRendererParams<OrderData>) {const allSelected = grid.state.rowDataSource.get().rowAreAllSelected();const selected = grid.state.rowSelectedIds.useValue();return (<div className="flex h-full w-full items-center justify-center"><GridCheckboxchecked={allSelected || selected.size > 0}indeterminate={!allSelected && selected.size > 0}onClick={(ev) => {ev.preventDefault();grid.api.rowSelectAll({ deselect: allSelected });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowSelectAll({ deselect: allSelected });}}/></div>);}export function MarkerCell({ grid, rowSelected }: CellRendererParams<OrderData>) {return (<div className="flex h-full w-full items-center justify-center"><GridCheckboxchecked={rowSelected}onClick={(ev) => {ev.stopPropagation();grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}/></div>);}export function GridCheckbox({children,indeterminate,...props}: C.CheckboxProps & { indeterminate?: boolean }) {return (<label className="text-md text-light flex items-center gap-2"><C.Root{...props}type="button"className={tw("bg-ln-gray-02 rounded border-transparent","shadow-[0_1.5px_2px_0_rgba(18,46,88,0.08),0_0_0_1px_var(--lng1771-gray-40)]","data-[state=checked]:bg-ln-primary-50 data-[state=checked]:shadow-[0_1.5px_2px_0_rgba(18,46,88,0.08),0_0_0_1px_var(--lng1771-primary-50)]","h-4 w-4",props.className,)}><C.CheckboxIndicator className={tw("flex items-center justify-center")}>{!indeterminate && <CheckIcon className="text-white dark:text-black" />}{indeterminate && <MinusIcon className="text-white dark:text-black" />}</C.CheckboxIndicator></C.Root>{children}</label>);}
The following example shows how to cancel row selection using this event. For more information about grid events, see the Grid Events guide.
<Grid.Rootgrid={grid}onRowSelectBegin={({ preventDefault, selected }) => {if (Number.parseInt(selected) % 2) {preventDefault();alert(`Selection of the row at index \${Number.parseInt(selected)} has been prevented.`);}}}>{/* other parts */}</Grid.Root>
Next Steps
- Row Pinning: Freeze specific rows at the top or bottom of the viewport.
- Row Full Width: Create rows that expand to the full width of the viewport.
- Row Detail: Render expandable row detail content, including nested grids.
- Row Sorting: Learn about the row sort model and how rows are ordered.
Row Pinning
LyteNyte Grid can freeze rows at the top or bottom of the grid viewport. Pinned rows remain visible as the user scrolls.
Row Detail (Master Detail)
Create expandable detail sections beneath rows in LyteNyte Grid with customizable React components, expansion controls, and programmatic API methods.