Column Resizing
Columns can be resized programmatically or through user interaction. LyteNyte Grid includes built-in drag-to-resize behavior for column headers.
Resizing Columns
Columns in LyteNyte Grid always have a fixed width. To resize a column, update
its fixed width. Users can drag the edge of a column header to change its width.
LyteNyte Grid supports this interaction out of the box. Enable it by setting
the resizable property to true on the column's uiHints property.
The example below enables resizing for all columns.
const grid = Grid.useLyteNyte({// ... other propertiescolumnBase: {uiHints: {resizable: true,},},});
When resizable is true, the grid renders a resize handle on the
header's edge. Users can drag this handle to change the width of a column.
The demo below enables the drag handle.
Try placing your cursor on the right edge of a column header
and dragging once the hover indicator appears.
Resizing Columns
"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,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 },{ id: "price", type: "number", cellRenderer: PriceCell, width: 100 },{ id: "customer", cellRenderer: AvatarCell, width: 180 },{ id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 120 },{ id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },{ id: "email", cellRenderer: EmailCell, width: 220 },];export default function ColumnBase() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,rowHeight: 50,columnBase: {uiHints: {resizable: true,},},});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 px-3 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="flex h-full w-full items-center px-3 text-sm"/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type { CellRendererParams } from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import { format } from "date-fns";import type { JSX, ReactNode } from "react";import type { OrderData } from "@1771technologies/grid-sample-data/orders";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>);
Dragging to resize does not work on touch devices because touch dragging scrolls the viewport. The developer should provide an alternative resize action, such as a “size to fit” button for accessibility purposes.
Customizing the Resize Handle
LyteNyte Grid renders the resize handle as an invisible div. You can style it
using CSS or by passing properties to the Grid.HeaderCell component.
Three properties customize the resize handle:
resizeStyle: Inline styles applied to the handle.resizeClassName: CSS class names applied to the handle.resizeAs: A render prop that supplies custom React content for the handle.
In most cases, resizeClassName is enough to customize the handle's appearance.
If you use a built-in grid theme, the handle already has styling. The default themes use the following CSS, which you can reference when implementing your own customizations:
[data-ln-header-resizer="true"] {background-color: transparent;transition: background-color 200ms ease-in-out;cursor: col-resize;&:hover {background-color: var(--lng1771-primary-50);}}
Resizing Programmatically
For one-off resizes or custom resize workflows, call the grid API's columnResize method.
It accepts a JavaScript object mapping column IDs to widths.
grid.api.columnResize({ exchange: 200, network: 100 });
The columnResize method is a convenience wrapper around columnUpdate. The following
code produces the same result:
grid.api.columnUpdate({ exchange: { width: 200 }, network: { width: 100 } });
Min & Max Column Width
Use the widthMin and widthMax properties to clamp a column's width.
By default, columns have a minimum width of 80px and a maximum width of 1000px.
The demo below clamps column widths between 100px and 300px.
When a user resizes a column past these bounds, LyteNyte Grid will use a clamped
width value for layout calculations.
Column Width Bounds
"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,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 },{ id: "price", type: "number", cellRenderer: PriceCell, width: 100 },{ id: "customer", cellRenderer: AvatarCell, width: 180 },{ id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 120 },{ id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },{ id: "email", cellRenderer: EmailCell, width: 220 },];export default function ColumnBase() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,rowHeight: 50,columnBase: {widthMax: 300,widthMin: 100,uiHints: {resizable: true,},},});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 px-3 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="flex h-full w-full items-center px-3 text-sm"/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type { CellRendererParams } from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import { format } from "date-fns";import type { JSX, ReactNode } from "react";import type { OrderData } from "@1771technologies/grid-sample-data/orders";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>);
The column widthMin and widthMax properties define visual layout bounds that
LyteNyte enforces. LyteNyte does not clamp the column's width value to
those limits, so your code can set width outside widthMin/widthMax. If width is out
of bounds and you later change widthMin or widthMax, LyteNyte immediately
resizes the column to fit the updated limits.
const column = { id: "exchange", widthMin: 100, width: 3000, widthMax: 300 };
Column Width Flex
Every column has a specified width or a default width. When the total column width is
smaller than the viewport, you can allocate the remaining space using widthFlex.
This property defines how much of the free space a column should take,
similar to the CSS flex property.
Full Width Viewport Filling
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useEffect, useId, useState } from "react";import type { OrderData } from "@1771technologies/grid-sample-data/orders";import { data } from "@1771technologies/grid-sample-data/orders";import {AvatarCell,IdCell,PaymentMethodCell,PriceCell,ProductCell,SwitchToggle,tw,} from "./components";const columns: Column<OrderData>[] = [{ id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },{ id: "product", cellRenderer: ProductCell, width: 200, widthFlex: 1 },{ id: "price", type: "number", cellRenderer: PriceCell, width: 100 },{ id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },{ id: "customer", cellRenderer: AvatarCell, width: 180, widthFlex: 1 },];export default function ColumnBase() {const ds = useClientRowDataSource({ data: data });const [widthFlex, setWidthFlex] = useState(true);const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,rowHeight: 50,});const view = grid.view.useValue();useEffect(() => {grid.state.columns.set((prev) => {return [...prev].map((x) => {if (x.id === "product" || x.id === "customer") {return { ...x, widthFlex: widthFlex ? 1 : 0 };}return x;});});}, [grid.state.columns, widthFlex]);return (<><div className="flex w-full border-b px-2 py-2"><SwitchTogglelabel="Toggle Width Flex"checked={widthFlex}onChange={() => {setWidthFlex((prev) => !prev);}}/></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 null;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("flex h-full w-full items-center text-nowrap px-3 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="flex h-full w-full items-center px-3 text-sm"/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div></>);}
import type { CellRendererParams } from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import { format } from "date-fns";import type { CSSProperties } from "react";import { useId, type JSX, type ReactNode } from "react";import type { OrderData } from "@1771technologies/grid-sample-data/orders";import { Switch } from "radix-ui";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 SwitchToggle(props: {label: string;checked: boolean;onChange: (b: boolean) => void;}) {const id = useId();return (<div className="flex items-center gap-2"><label className="text-ln-gray-90 text-sm leading-none" htmlFor={id}>{props.label}</label><Switch.RootclassName="bg-ln-gray-10 data-[state=checked]:bg-ln-gray-40 relative h-[22px] w-[38px] cursor-pointer rounded-full border outline-none"id={id}checked={props.checked}onCheckedChange={(c) => props.onChange(c)}style={{ "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)" } as CSSProperties}><Switch.Thumb className="block size-[18px] translate-x-[2px] rounded-full bg-white/95 shadow transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[16px] data-[state=checked]:bg-white" /></Switch.Root></div>);}
widthFlex is most noticeable on wide viewports. On small screens, you may not see
columns expand. View this demo on a desktop-sized display to see the effect.
Size To Fit
The columnSizeToFit property sizes columns so they fit within the viewport.
When total column width exceeds the viewport, the grid shrinks column widths.
When total width is smaller, the grid expands them.
In the demo below, you can toggle the columnSizeToFit property to observe how
it changes the widths of the column. The smaller the size of your display the
more pronounced the effect will be.
Size-to-Fit Columns
"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,SwitchToggle,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, hide: true, 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 ColumnBase() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 80 },columnSizeToFit: true,});const view = grid.view.useValue();return (<div><div className="flex w-full border-b px-2 py-2"><SwitchTogglelabel="Toggle Size To Fit"checked={grid.state.columnSizeToFit.useValue()}onChange={() => {grid.state.columnSizeToFit.set((prev) => !prev);}}/></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 null;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";import type { CSSProperties } from "react";import { useId } from "react";import { Switch } from "radix-ui";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>);};};export function SwitchToggle(props: {label: string;checked: boolean;onChange: (b: boolean) => void;}) {const id = useId();return (<div className="flex items-center gap-2"><label className="text-ln-gray-90 text-sm leading-none" htmlFor={id}>{props.label}</label><Switch.RootclassName="bg-ln-gray-10 data-[state=checked]:bg-ln-gray-40 relative h-[22px] w-[38px] cursor-pointer rounded-full border outline-none"id={id}checked={props.checked}onCheckedChange={(c) => props.onChange(c)}style={{ "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)" } as CSSProperties}><Switch.Thumb className="block size-[18px] translate-x-[2px] rounded-full bg-white/95 shadow transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[16px] data-[state=checked]:bg-white" /></Switch.Root></div>);}
Use the columnSizeToFit property sparingly. Columns can appear compressed on
narrow viewports, and cell content may truncate. Only enable this feature
when all columns must be visible.
Measure Text Function
LyteNyte Grid exports a measureText function that approximates the
pixel width of a string. This is useful when adjusting cell
content based on available space.
Measuring the width of a given cell's text, allows for some interesting use cases.
For example, applications like Excel show # symbols when a number overflows the cell.
The demo below demonstrates the pattern by toggling this effect on and off.
- String Truncation: The Symbol and Exchange columns, which contain strings, are truncated.
- Number Hashing: The Performance and Volatility columns, which contain numbers, are hashed.
Truncating and Hashing
"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,SwitchToggle,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, hide: true, 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 ColumnBase() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 90 },columnSizeToFit: true,});const view = grid.view.useValue();return (<div><div className="flex w-full border-b px-2 py-2"><SwitchTogglelabel="Toggle Hash Truncate Cells"checked={grid.state.columnSizeToFit.useValue()}onChange={() => {grid.state.columnSizeToFit.set((prev) => !prev);}}/></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 null;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,GridState,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";import type { CSSProperties } from "react";import { useId, useMemo } from "react";import { Switch } from "radix-ui";import { measureText } from "@1771technologies/lytenyte-pro";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, state },column,colIndex,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",)}><HashedValue value={value} gridState={state} columnIndex={colIndex} /></div>);}export function PercentCell({grid: { api, state },colIndex,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="flex h-full w-full items-center justify-end tabular-nums"><HashedValue value={value} gridState={state} columnIndex={colIndex} /></div>);}function HashedValue({value,columnIndex,gridState,}: {value: string;gridState: GridState<DEXPerformanceData>;columnIndex: number;}) {const widths = gridState.xPositions.useValue();const width = widths[columnIndex + 1] - widths[columnIndex];const measuredWidth = useMemo(() => {const m = measureText(value, gridState.viewport.get()!);return m.width;}, [gridState.viewport, value]);// Minus 6 as slight adjustment so we know there is definitely enough spaceif (measuredWidth > width - 6) return "#".repeat(Math.ceil(width / 16) + 1);return value;}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>);};};export function SwitchToggle(props: {label: string;checked: boolean;onChange: (b: boolean) => void;}) {const id = useId();return (<div className="flex items-center gap-2"><label className="text-ln-gray-90 text-sm leading-none" htmlFor={id}>{props.label}</label><Switch.RootclassName="bg-ln-gray-10 data-[state=checked]:bg-ln-gray-40 relative h-[22px] w-[38px] cursor-pointer rounded-full border outline-none"id={id}checked={props.checked}onCheckedChange={(c) => props.onChange(c)}style={{ "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)" } as CSSProperties}><Switch.Thumb className="block size-[18px] translate-x-[2px] rounded-full bg-white/95 shadow transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[16px] data-[state=checked]:bg-white" /></Switch.Root></div>);}
In the demo, the HashedValue component uses measureText inside a useMemo
call. Measuring text is expensive, so caching the result avoids running the calculation on every render.
function HashedValue({value,columnIndex,gridState,}: {value: string;gridState: GridState<DEXPerformanceData>;columnIndex: number;}) {const widths = gridState.xPositions.useValue();const width = widths[columnIndex + 1] - widths[columnIndex];const measuredWidth = useMemo(() => {const m = measureText(value, gridState.viewport.get()!);return m.width;}, [gridState.viewport, value]);if (measuredWidth > width) return "#".repeat(Math.ceil(width / 16) + 1);return value;}
The measureText function has many uses. LyteNyte Grid relies on it internally for autosizing and
other text-based measurements. Remember that the function returns an approximation, not
an exact value. Differences in browser text rendering prevent pixel-perfect consistency.
Next Steps
- Column Base: See the default column values and how to override them with the
columnBaseproperty. - Column ID & Name: Define user-friendly column names and ensure unique IDs.
- Column Moving: Reorder columns programmatically or through drag-and-drop.
- Column Field: Control how a column retrieves its value for each cell.
Column Visibility
Learn how to manage column visibility in LyteNyte Grid and understand the difference between hidden columns and collapsed column groups.
Column Field
A column's field determines how LyteNyte Grid retrieves a cell value. This guide explains the four field types supported by LyteNyte Grid.