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
the column’s width property. Users can drag the edge of a column header
to change the column’s width. LyteNyte Grid supports this interaction out of the box.
Enable it by setting the resizable property to true on the column specification.
You can make all columns resizable by setting resizable on the base column.
1<Grid columnBase={{ 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
15 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import type { OrderData } from "@1771technologies/grid-sample-data/orders";3import { data } from "@1771technologies/grid-sample-data/orders";4import {5 AvatarCell,6 EmailCell,7 IdCell,8 PaymentMethodCell,9 PriceCell,10 ProductCell,11 PurchaseDateCell,12} from "./components.jsx";13import { useClientDataSource, Grid } from "@1771technologies/lytenyte-pro";14import { useState } from "react";15import { ViewportShadows } from "@1771technologies/lytenyte-pro/components";16
17export interface GridSpec {18 readonly data: OrderData;19}20
21const base: Grid.ColumnBase<GridSpec> = { resizable: true };22
23export default function ColumnDemo() {24 const ds = useClientDataSource({ data: data });25 const [columns, setColumns] = useState<Grid.Column<GridSpec>[]>([26 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },27 { id: "product", cellRenderer: ProductCell, width: 200, name: "Product" },28 { id: "price", type: "number", cellRenderer: PriceCell, width: 100, name: "Price" },29 { id: "customer", cellRenderer: AvatarCell, width: 180, name: "Customer" },30 { id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 130 },31 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },32 { id: "email", cellRenderer: EmailCell, width: 220, name: "Email" },33 ]);34
35 return (36 <div className={"ln-grid"} style={{ height: 500 }}>37 <Grid38 rowHeight={50}39 columns={columns}40 onColumnsChange={setColumns}41 rowSource={ds}42 columnBase={base}43 slotShadows={ViewportShadows}44 />45 </div>46 );47}1import { format } from "date-fns";2import { type JSX, type ReactNode } from "react";3import type { Grid } from "@1771technologies/lytenyte-pro";4import type { GridSpec } from "./demo.jsx";5
6export function ProductCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {7 if (!api.rowIsLeaf(row) || !row.data) return;8
9 const url = row.data?.productThumbnail;10 const title = row.data.product;11 const desc = row.data.productDescription;12
13 return (14 <div className="flex h-full w-full items-center gap-2">15 <img className="border-ln-border-strong h-7 w-7 rounded-lg border" src={url} alt={title + desc} />16 <div className="text-ln-text-dark flex flex-col gap-0.5">17 <div className="font-semibold">{title}</div>18 <div className="text-ln-text-light text-xs">{desc}</div>19 </div>20 </div>21 );22}23
24export function AvatarCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {25 if (!api.rowIsLeaf(row) || !row.data) return;26
27 const url = row.data?.customerAvatar;28
29 const name = row.data.customer;30
31 return (32 <div className="flex h-full w-full items-center gap-2">33 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />34 <div className="text-ln-text-dark flex flex-col gap-0.5">35 <div>{name}</div>36 </div>37 </div>38 );39}40
41const formatter = new Intl.NumberFormat("en-Us", {42 minimumFractionDigits: 2,43 maximumFractionDigits: 2,44});45export function PriceCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {46 if (!api.rowIsLeaf(row) || !row.data) return;47
48 const price = formatter.format(row.data.price);49 const [dollars, cents] = price.split(".");50
51 return (52 <div className="flex h-full w-full items-center justify-end">53 <div className="flex items-baseline tabular-nums">54 <span className="text-ln-text font-semibold">${dollars}</span>.55 <span className="relative text-xs">{cents}</span>56 </div>57 </div>58 );59}60
61export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {62 if (!api.rowIsLeaf(row) || !row.data) return;63
64 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");65
66 return <div className="flex h-full w-full items-center">{formattedDate}</div>;67}68export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {69 if (!api.rowIsLeaf(row) || !row.data) return;70
71 return <div className="text-xs tabular-nums">{row.data.id}</div>;72}73
74export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {75 if (!api.rowIsLeaf(row) || !row.data) return;76
77 const cardNumber = row.data.cardNumber;78 const provider = row.data.paymentMethod;79
80 let Logo: ReactNode = null;81 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;82 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;83
84 return (85 <div className="flex h-full w-full items-center gap-2">86 <div className="flex w-7 items-center justify-center">{Logo}</div>87 <div className="flex items-center gap-px">88 <div className="bg-ln-gray-40 size-2 rounded-full"></div>89 <div className="bg-ln-gray-40 size-2 rounded-full"></div>90 <div className="bg-ln-gray-40 size-2 rounded-full"></div>91 <div className="bg-ln-gray-40 size-2 rounded-full"></div>92 </div>93 <div className="tabular-nums">{cardNumber}</div>94 </div>95 );96}97
98export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return;100
101 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;102}103
104const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (105 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>106 <path107 fill="#1434cb"108 d="M651.185.5c-70.933 0-134.322 36.766-134.322 104.694 0 77.9 112.423 83.28 112.423 122.415 0 16.478-18.884 31.229-51.137 31.229-45.773 0-79.984-20.611-79.984-20.611l-14.638 68.547s39.41 17.41 91.734 17.41c77.552 0 138.576-38.572 138.576-107.66 0-82.316-112.89-87.537-112.89-123.86 0-12.91 15.501-27.053 47.662-27.053 36.286 0 65.892 14.99 65.892 14.99l14.326-66.204S696.614.5 651.185.5zM2.218 5.497.5 15.49s29.842 5.461 56.719 16.356c34.606 12.492 37.072 19.765 42.9 42.353l63.51 244.832h85.138L379.927 5.497h-84.942L210.707 218.67l-34.39-180.696c-3.154-20.68-19.13-32.477-38.685-32.477H2.218zm411.865 0L347.449 319.03h80.999l66.4-313.534h-80.765zm451.759 0c-19.532 0-29.88 10.457-37.474 28.73L709.699 319.03h84.942l16.434-47.468h103.483l9.994 47.468H999.5L934.115 5.497h-68.273zm11.047 84.707 25.178 117.653h-67.454z"109 />110 </svg>111);112
113const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (114 <svg115 xmlns="http://www.w3.org/2000/svg"116 width={2500}117 height={1524}118 viewBox="55.2 38.3 464.5 287.8"119 {...props}120 >121 <path122 fill="#f79f1a"123 d="M519.7 182.2c0 79.5-64.3 143.9-143.6 143.9s-143.6-64.4-143.6-143.9S296.7 38.3 376 38.3s143.7 64.4 143.7 143.9z"124 />125 <path126 fill="#ea001b"127 d="M342.4 182.2c0 79.5-64.3 143.9-143.6 143.9S55.2 261.7 55.2 182.2 119.5 38.3 198.8 38.3s143.6 64.4 143.6 143.9z"128 />129 <path130 fill="#ff5f01"131 d="M287.4 68.9c-33.5 26.3-55 67.3-55 113.3s21.5 87 55 113.3c33.5-26.3 55-67.3 55-113.3s-21.5-86.9-55-113.3z"132 />133 </svg>134);Warning
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.
Two properties customize the resize handle:
resizeStyle: Inline styles applied to the handle.resizeClassName: CSS class names applied to 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:
1[data-ln-header-resizer="true"] {2 background-color: transparent;3 transition: background-color 200ms ease-in-out;4 cursor: col-resize;5 &:hover {6 background-color: var(--ln-primary-50);7 }8}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.
1api.columnResize({ exchange: 200, network: 100 });The columnResize method is a convenience wrapper around columnUpdate. The following
code produces the same result:
1api.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
15 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import type { OrderData } from "@1771technologies/grid-sample-data/orders";3import { data } from "@1771technologies/grid-sample-data/orders";4import {5 AvatarCell,6 EmailCell,7 IdCell,8 PaymentMethodCell,9 PriceCell,10 ProductCell,11 PurchaseDateCell,12} from "./components.jsx";13import { useClientDataSource, Grid } from "@1771technologies/lytenyte-pro";14import { useState } from "react";15import { ViewportShadows } from "@1771technologies/lytenyte-pro/components";16
17export interface GridSpec {18 readonly data: OrderData;19}20
21const base: Grid.ColumnBase<GridSpec> = { resizable: true, widthMin: 100, widthMax: 300 };22
23export default function ColumnDemo() {24 const ds = useClientDataSource({ data: data });25 const [columns, setColumns] = useState<Grid.Column<GridSpec>[]>([26 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },27 { id: "product", cellRenderer: ProductCell, width: 200, name: "Product" },28 { id: "price", type: "number", cellRenderer: PriceCell, width: 100, name: "Price" },29 { id: "customer", cellRenderer: AvatarCell, width: 180, name: "Customer" },30 { id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 130 },31 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },32 { id: "email", cellRenderer: EmailCell, width: 220, name: "Email" },33 ]);34
35 return (36 <div className={"ln-grid"} style={{ height: 500 }}>37 <Grid38 rowHeight={50}39 columns={columns}40 onColumnsChange={setColumns}41 rowSource={ds}42 columnBase={base}43 slotShadows={ViewportShadows}44 />45 </div>46 );47}1import { format } from "date-fns";2import { type JSX, type ReactNode } from "react";3import type { Grid } from "@1771technologies/lytenyte-pro";4import type { GridSpec } from "./demo.jsx";5
6export function ProductCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {7 if (!api.rowIsLeaf(row) || !row.data) return;8
9 const url = row.data?.productThumbnail;10 const title = row.data.product;11 const desc = row.data.productDescription;12
13 return (14 <div className="flex h-full w-full items-center gap-2">15 <img className="border-ln-border-strong h-7 w-7 rounded-lg border" src={url} alt={title + desc} />16 <div className="text-ln-text-dark flex flex-col gap-0.5">17 <div className="font-semibold">{title}</div>18 <div className="text-ln-text-light text-xs">{desc}</div>19 </div>20 </div>21 );22}23
24export function AvatarCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {25 if (!api.rowIsLeaf(row) || !row.data) return;26
27 const url = row.data?.customerAvatar;28
29 const name = row.data.customer;30
31 return (32 <div className="flex h-full w-full items-center gap-2">33 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />34 <div className="text-ln-text-dark flex flex-col gap-0.5">35 <div>{name}</div>36 </div>37 </div>38 );39}40
41const formatter = new Intl.NumberFormat("en-Us", {42 minimumFractionDigits: 2,43 maximumFractionDigits: 2,44});45export function PriceCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {46 if (!api.rowIsLeaf(row) || !row.data) return;47
48 const price = formatter.format(row.data.price);49 const [dollars, cents] = price.split(".");50
51 return (52 <div className="flex h-full w-full items-center justify-end">53 <div className="flex items-baseline tabular-nums">54 <span className="text-ln-text font-semibold">${dollars}</span>.55 <span className="relative text-xs">{cents}</span>56 </div>57 </div>58 );59}60
61export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {62 if (!api.rowIsLeaf(row) || !row.data) return;63
64 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");65
66 return <div className="flex h-full w-full items-center">{formattedDate}</div>;67}68export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {69 if (!api.rowIsLeaf(row) || !row.data) return;70
71 return <div className="text-xs tabular-nums">{row.data.id}</div>;72}73
74export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {75 if (!api.rowIsLeaf(row) || !row.data) return;76
77 const cardNumber = row.data.cardNumber;78 const provider = row.data.paymentMethod;79
80 let Logo: ReactNode = null;81 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;82 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;83
84 return (85 <div className="flex h-full w-full items-center gap-2">86 <div className="flex w-7 items-center justify-center">{Logo}</div>87 <div className="flex items-center gap-px">88 <div className="bg-ln-gray-40 size-2 rounded-full"></div>89 <div className="bg-ln-gray-40 size-2 rounded-full"></div>90 <div className="bg-ln-gray-40 size-2 rounded-full"></div>91 <div className="bg-ln-gray-40 size-2 rounded-full"></div>92 </div>93 <div className="tabular-nums">{cardNumber}</div>94 </div>95 );96}97
98export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return;100
101 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;102}103
104const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (105 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>106 <path107 fill="#1434cb"108 d="M651.185.5c-70.933 0-134.322 36.766-134.322 104.694 0 77.9 112.423 83.28 112.423 122.415 0 16.478-18.884 31.229-51.137 31.229-45.773 0-79.984-20.611-79.984-20.611l-14.638 68.547s39.41 17.41 91.734 17.41c77.552 0 138.576-38.572 138.576-107.66 0-82.316-112.89-87.537-112.89-123.86 0-12.91 15.501-27.053 47.662-27.053 36.286 0 65.892 14.99 65.892 14.99l14.326-66.204S696.614.5 651.185.5zM2.218 5.497.5 15.49s29.842 5.461 56.719 16.356c34.606 12.492 37.072 19.765 42.9 42.353l63.51 244.832h85.138L379.927 5.497h-84.942L210.707 218.67l-34.39-180.696c-3.154-20.68-19.13-32.477-38.685-32.477H2.218zm411.865 0L347.449 319.03h80.999l66.4-313.534h-80.765zm451.759 0c-19.532 0-29.88 10.457-37.474 28.73L709.699 319.03h84.942l16.434-47.468h103.483l9.994 47.468H999.5L934.115 5.497h-68.273zm11.047 84.707 25.178 117.653h-67.454z"109 />110 </svg>111);112
113const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (114 <svg115 xmlns="http://www.w3.org/2000/svg"116 width={2500}117 height={1524}118 viewBox="55.2 38.3 464.5 287.8"119 {...props}120 >121 <path122 fill="#f79f1a"123 d="M519.7 182.2c0 79.5-64.3 143.9-143.6 143.9s-143.6-64.4-143.6-143.9S296.7 38.3 376 38.3s143.7 64.4 143.7 143.9z"124 />125 <path126 fill="#ea001b"127 d="M342.4 182.2c0 79.5-64.3 143.9-143.6 143.9S55.2 261.7 55.2 182.2 119.5 38.3 198.8 38.3s143.6 64.4 143.6 143.9z"128 />129 <path130 fill="#ff5f01"131 d="M287.4 68.9c-33.5 26.3-55 67.3-55 113.3s21.5 87 55 113.3c33.5-26.3 55-67.3 55-113.3s-21.5-86.9-55-113.3z"132 />133 </svg>134);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.
1const 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
14 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import type { OrderData } from "@1771technologies/grid-sample-data/orders";3import { data } from "@1771technologies/grid-sample-data/orders";4import {5 AvatarCell,6 IdCell,7 PaymentMethodCell,8 PriceCell,9 ProductCell,10 SwitchToggle,11} from "./components.jsx";12import { useClientDataSource, Grid } from "@1771technologies/lytenyte-pro";13import { useMemo, useState } from "react";14import { ViewportShadows } from "@1771technologies/lytenyte-pro/components";15
16export interface GridSpec {17 readonly data: OrderData;18}19
20const columns: Grid.Column<GridSpec>[] = [21 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },22 { id: "product", cellRenderer: ProductCell, width: 200, name: "Product" },23 { id: "price", type: "number", cellRenderer: PriceCell, width: 100, name: "Price" },24 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },25 { id: "customer", cellRenderer: AvatarCell, width: 180, name: "Customer" },26];27
28const base: Grid.ColumnBase<GridSpec> = { widthMin: 100, widthMax: 300 };29
30export default function ColumnDemo() {31 const ds = useClientDataSource({ data: data });32 const [widthFlex, setWidthFlex] = useState(true);33
34 const columnBase = useMemo(() => {35 if (widthFlex) return { ...base, widthFlex: 1 };36 return base;37 }, [widthFlex]);38
39 return (40 <div className={"ln-grid"} style={{ height: 500 }}>41 <div className="border-ln-border flex w-full border-b px-2 py-2">42 <SwitchToggle43 label="Toggle Width Flex"44 checked={widthFlex}45 onChange={() => {46 setWidthFlex((prev) => !prev);47 }}48 />49 </div>50 <Grid51 rowHeight={50}52 columns={columns}53 rowSource={ds}54 columnBase={columnBase}55 slotShadows={ViewportShadows}56 />57 </div>58 );59}1import { format } from "date-fns";2import { useId, type CSSProperties, type JSX, type ReactNode } from "react";3import type { Grid } from "@1771technologies/lytenyte-pro";4import type { GridSpec } from "./demo.jsx";5import { Switch } from "radix-ui";6
7export function ProductCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {8 if (!api.rowIsLeaf(row) || !row.data) return;9
10 const url = row.data?.productThumbnail;11 const title = row.data.product;12 const desc = row.data.productDescription;13
14 return (15 <div className="flex h-full w-full items-center gap-2">16 <img className="border-ln-border-strong h-7 w-7 rounded-lg border" src={url} alt={title + desc} />17 <div className="text-ln-text-dark flex flex-col gap-0.5">18 <div className="font-semibold">{title}</div>19 <div className="text-ln-text-light text-xs">{desc}</div>20 </div>21 </div>22 );23}24
25export function AvatarCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {26 if (!api.rowIsLeaf(row) || !row.data) return;27
28 const url = row.data?.customerAvatar;29
30 const name = row.data.customer;31
32 return (33 <div className="flex h-full w-full items-center gap-2">34 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />35 <div className="text-ln-text-dark flex flex-col gap-0.5">36 <div>{name}</div>37 </div>38 </div>39 );40}41
42const formatter = new Intl.NumberFormat("en-Us", {43 minimumFractionDigits: 2,44 maximumFractionDigits: 2,45});46export function PriceCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {47 if (!api.rowIsLeaf(row) || !row.data) return;48
49 const price = formatter.format(row.data.price);50 const [dollars, cents] = price.split(".");51
52 return (53 <div className="flex h-full w-full items-center justify-end">54 <div className="flex items-baseline tabular-nums">55 <span className="text-ln-text font-semibold">${dollars}</span>.56 <span className="relative text-xs">{cents}</span>57 </div>58 </div>59 );60}61
62export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {63 if (!api.rowIsLeaf(row) || !row.data) return;64
65 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");66
67 return <div className="flex h-full w-full items-center">{formattedDate}</div>;68}69export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {70 if (!api.rowIsLeaf(row) || !row.data) return;71
72 return <div className="text-xs tabular-nums">{row.data.id}</div>;73}74
75export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {76 if (!api.rowIsLeaf(row) || !row.data) return;77
78 const cardNumber = row.data.cardNumber;79 const provider = row.data.paymentMethod;80
81 let Logo: ReactNode = null;82 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;83 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;84
85 return (86 <div className="flex h-full w-full items-center gap-2">87 <div className="flex w-7 items-center justify-center">{Logo}</div>88 <div className="flex items-center gap-px">89 <div className="bg-ln-gray-40 size-2 rounded-full"></div>90 <div className="bg-ln-gray-40 size-2 rounded-full"></div>91 <div className="bg-ln-gray-40 size-2 rounded-full"></div>92 <div className="bg-ln-gray-40 size-2 rounded-full"></div>93 </div>94 <div className="tabular-nums">{cardNumber}</div>95 </div>96 );97}98
99export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {100 if (!api.rowIsLeaf(row) || !row.data) return;101
102 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;103}104
105const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (106 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>107 <path108 fill="#1434cb"109 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"110 />111 </svg>112);113
114const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (115 <svg116 xmlns="http://www.w3.org/2000/svg"117 width={2500}118 height={1524}119 viewBox="55.2 38.3 464.5 287.8"120 {...props}121 >122 <path123 fill="#f79f1a"124 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"125 />126 <path127 fill="#ea001b"128 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"129 />130 <path131 fill="#ff5f01"132 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"133 />134 </svg>135);136
137export function SwitchToggle(props: { label: string; checked: boolean; onChange: (b: boolean) => void }) {138 const id = useId();139 return (140 <div className="flex items-center gap-2">141 <label className="text-ln-text-dark text-sm leading-none" htmlFor={id}>142 {props.label}143 </label>144 <Switch.Root145 className="bg-ln-gray-10 data-[state=checked]:bg-ln-primary-50 h-5.5 w-9.5 border-ln-border-strong relative cursor-pointer rounded-full border outline-none"146 id={id}147 checked={props.checked}148 onCheckedChange={(c) => props.onChange(c)}149 style={{ "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)" } as CSSProperties}150 >151 <Switch.Thumb className="size-4.5 block translate-x-0.5 rounded-full bg-white/95 shadow transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white" />152 </Switch.Root>153 </div>154 );155}Info
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
79 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";3import {4 ExchangeCell,5 makePerfHeaderCell,6 NetworkCell,7 PercentCell,8 PercentCellPositiveNegative,9 SwitchToggle,10 SymbolCell,11} from "./components.jsx";12import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";13import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { useState } from "react";15
16export interface GridSpec {17 readonly data: DEXPerformanceData;18}19
20const columns: Grid.Column<GridSpec>[] = [21 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },22 { id: "network", cellRenderer: NetworkCell, width: 220, hide: true, name: "Network" },23 { id: "exchange", cellRenderer: ExchangeCell, width: 220, name: "Exchange" },24
25 {26 id: "change24h",27 cellRenderer: PercentCellPositiveNegative,28 headerRenderer: makePerfHeaderCell("Change", "24h"),29 name: "Change % 24h",30 type: "number,",31 },32
33 {34 id: "perf1w",35 cellRenderer: PercentCellPositiveNegative,36 headerRenderer: makePerfHeaderCell("Perf %", "1w"),37 name: "Perf % 1W",38 type: "number,",39 },40 {41 id: "perf1m",42 cellRenderer: PercentCellPositiveNegative,43 headerRenderer: makePerfHeaderCell("Perf %", "1m"),44 name: "Perf % 1M",45 type: "number,",46 },47 {48 id: "perf3m",49 cellRenderer: PercentCellPositiveNegative,50 headerRenderer: makePerfHeaderCell("Perf %", "3m"),51 name: "Perf % 3M",52 type: "number,",53 },54 {55 id: "perf6m",56 cellRenderer: PercentCellPositiveNegative,57 headerRenderer: makePerfHeaderCell("Perf %", "6m"),58 name: "Perf % 6M",59 type: "number,",60 },61 {62 id: "perfYtd",63 cellRenderer: PercentCellPositiveNegative,64 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),65 name: "Perf % YTD",66 type: "number",67 },68 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },69 {70 id: "volatility1m",71 cellRenderer: PercentCell,72 headerRenderer: makePerfHeaderCell("Volatility", "1m"),73 name: "Volatility 1M",74 type: "number",75 },76];77
78const base: Grid.ColumnBase<GridSpec> = { width: 80 };79
80export default function ColumnDemo() {81 const ds = useClientDataSource({ data: data });82 const [sizeToFit, setSizeToFit] = useState(true);83
84 return (85 <>86 <div className="border-ln-border flex w-full border-b px-2 py-2">87 <SwitchToggle88 label="Toggle Size To Fit"89 checked={sizeToFit}90 onChange={() => {91 setSizeToFit((prev) => !prev);92 }}93 />94 </div>95 <div96 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight"97 style={{ height: 500 }}98 >99 <Grid columns={columns} columnBase={base} rowSource={ds} columnSizeToFit={sizeToFit} />100 </div>101 </>102 );103}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { useId, type CSSProperties } from "react";12import { Switch } from "radix-ui";13
14export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {15 if (!api.rowIsLeaf(row) || !row.data) return null;16
17 const ticker = row.data.symbolTicker;18 const symbol = row.data.symbol;19 const image = symbols[row.data.symbolTicker];20
21 return (22 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">23 <div>24 <img25 src={image}26 alt={`Logo for symbol ${symbol}`}27 className="h-full w-full overflow-hidden rounded-full"28 />29 </div>30 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">31 {ticker}32 </div>33 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>34 </div>35 );36}37
38export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {39 if (!api.rowIsLeaf(row) || !row.data) return null;40
41 const name = row.data.network;42 const image = networks[name];43
44 return (45 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">46 <div>47 <img48 src={image}49 alt={`Logo for network ${name}`}50 className="h-full w-full overflow-hidden rounded-full"51 />52 </div>53 <div className="w-full overflow-hidden text-ellipsis">{name}</div>54 </div>55 );56}57
58export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {59 if (!api.rowIsLeaf(row) || !row.data) return null;60
61 const name = row.data.exchange;62 const image = exchanges[name];63
64 return (65 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">66 <div>67 <img68 src={image}69 alt={`Logo for exchange ${name}`}70 className="h-full w-full overflow-hidden rounded-full"71 />72 </div>73 <div className="w-full overflow-hidden text-ellipsis">{name}</div>74 </div>75 );76}77
78export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {79 if (!api.rowIsLeaf(row) || !row.data) return null;80
81 const field = api.columnField(column, row);82
83 if (typeof field !== "number") return "-";84
85 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";86
87 return (88 <div89 className={tw(90 "h-ful flex w-full items-center justify-end tabular-nums",91 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",92 )}93 >94 {value}95 </div>96 );97}98
99export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {100 if (!api.rowIsLeaf(row) || !row.data) return null;101
102 const field = api.columnField(column, row);103
104 if (typeof field !== "number") return "-";105
106 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";107
108 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;109}110
111export const makePerfHeaderCell = (name: string, subname: string) => {112 return (_: Grid.T.HeaderParams<GridSpec>) => {113 return (114 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">115 <div>{name}</div>116 <div className="text-ln-text-light font-mono uppercase">{subname}</div>117 </div>118 );119 };120};121
122export function SwitchToggle(props: { label: string; checked: boolean; onChange: (b: boolean) => void }) {123 const id = useId();124 return (125 <div className="flex items-center gap-2">126 <label className="text-ln-text-dark text-sm leading-none" htmlFor={id}>127 {props.label}128 </label>129 <Switch.Root130 className="bg-ln-gray-10 data-[state=checked]:bg-ln-primary-50 h-5.5 w-9.5 border-ln-border-strong relative cursor-pointer rounded-full border outline-none"131 id={id}132 checked={props.checked}133 onCheckedChange={(c) => props.onChange(c)}134 style={{ "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)" } as CSSProperties}135 >136 <Switch.Thumb className="size-4.5 block translate-x-0.5 rounded-full bg-white/95 shadow transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white" />137 </Switch.Root>138 </div>139 );140}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
79 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";3import {4 ExchangeCell,5 makePerfHeaderCell,6 NetworkCell,7 PercentCell,8 PercentCellPositiveNegative,9 SwitchToggle,10 SymbolCell,11} from "./components.jsx";12import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";13import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { useState } from "react";15
16export interface GridSpec {17 readonly data: DEXPerformanceData;18}19
20const columns: Grid.Column<GridSpec>[] = [21 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },22 { id: "network", cellRenderer: NetworkCell, width: 220, name: "Network" },23 { id: "exchange", cellRenderer: ExchangeCell, width: 220, hide: true, name: "Exchange" },24
25 {26 id: "change24h",27 cellRenderer: PercentCellPositiveNegative,28 headerRenderer: makePerfHeaderCell("Change", "24h"),29 name: "Change % 24h",30 type: "number,",31 },32
33 {34 id: "perf1w",35 cellRenderer: PercentCellPositiveNegative,36 headerRenderer: makePerfHeaderCell("Perf %", "1w"),37 name: "Perf % 1W",38 type: "number,",39 },40 {41 id: "perf1m",42 cellRenderer: PercentCellPositiveNegative,43 headerRenderer: makePerfHeaderCell("Perf %", "1m"),44 name: "Perf % 1M",45 type: "number,",46 },47 {48 id: "perf3m",49 cellRenderer: PercentCellPositiveNegative,50 headerRenderer: makePerfHeaderCell("Perf %", "3m"),51 name: "Perf % 3M",52 type: "number,",53 },54 {55 id: "perf6m",56 cellRenderer: PercentCellPositiveNegative,57 headerRenderer: makePerfHeaderCell("Perf %", "6m"),58 name: "Perf % 6M",59 type: "number,",60 },61 {62 id: "perfYtd",63 cellRenderer: PercentCellPositiveNegative,64 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),65 name: "Perf % YTD",66 type: "number",67 },68 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },69 {70 id: "volatility1m",71 cellRenderer: PercentCell,72 headerRenderer: makePerfHeaderCell("Volatility", "1m"),73 name: "Volatility 1M",74 type: "number",75 },76];77
78const base: Grid.ColumnBase<GridSpec> = { width: 80 };79
80export default function ColumnDemo() {81 const ds = useClientDataSource({ data: data });82 const [sizeToFit, setSizeToFit] = useState(true);83
84 return (85 <>86 <div className="border-ln-border flex w-full border-b px-2 py-2">87 <SwitchToggle88 label="Toggle Hash Truncate Cells"89 checked={sizeToFit}90 onChange={() => {91 setSizeToFit((prev) => !prev);92 }}93 />94 </div>95 <div96 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight"97 style={{ height: 500 }}98 >99 <Grid columns={columns} columnBase={base} rowSource={ds} columnSizeToFit={sizeToFit} />100 </div>101 </>102 );103}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import { measureText, type Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { useId, useMemo, type CSSProperties } from "react";12import { Switch } from "radix-ui";13
14export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {15 if (!api.rowIsLeaf(row) || !row.data) return null;16
17 const ticker = row.data.symbolTicker;18 const symbol = row.data.symbol;19 const image = symbols[row.data.symbolTicker];20
21 return (22 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">23 <div>24 <img25 src={image}26 alt={`Logo for symbol ${symbol}`}27 className="h-full w-full overflow-hidden rounded-full"28 />29 </div>30 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">31 {ticker}32 </div>33 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>34 </div>35 );36}37
38export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {39 if (!api.rowIsLeaf(row) || !row.data) return null;40
41 const name = row.data.network;42 const image = networks[name];43
44 return (45 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">46 <div>47 <img48 src={image}49 alt={`Logo for network ${name}`}50 className="h-full w-full overflow-hidden rounded-full"51 />52 </div>53 <div className="w-full overflow-hidden text-ellipsis">{name}</div>54 </div>55 );56}57
58export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {59 if (!api.rowIsLeaf(row) || !row.data) return null;60
61 const name = row.data.exchange;62 const image = exchanges[name];63
64 return (65 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">66 <div>67 <img68 src={image}69 alt={`Logo for exchange ${name}`}70 className="h-full w-full overflow-hidden rounded-full"71 />72 </div>73 <div className="w-full overflow-hidden text-ellipsis">{name}</div>74 </div>75 );76}77
78export function PercentCellPositiveNegative({79 api,80 column,81 colIndex,82 row,83}: Grid.T.CellRendererParams<GridSpec>) {84 if (!api.rowIsLeaf(row) || !row.data) return null;85
86 const field = api.columnField(column, row);87
88 if (typeof field !== "number") return "-";89
90 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";91
92 return (93 <div94 className={tw(95 "h-ful flex w-full items-center justify-end tabular-nums",96 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",97 )}98 >99 <HashedValue value={value} api={api} columnIndex={colIndex} />100 </div>101 );102}103
104export function PercentCell({ api, colIndex, column, row }: Grid.T.CellRendererParams<GridSpec>) {105 if (!api.rowIsLeaf(row) || !row.data) return null;106
107 const field = api.columnField(column, row);108
109 if (typeof field !== "number") return "-";110
111 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";112
113 return (114 <div className="flex h-full w-full items-center justify-end tabular-nums">115 <HashedValue value={value} api={api} columnIndex={colIndex} />116 </div>117 );118}119
120function HashedValue({121 value,122 columnIndex,123 api,124}: {125 value: string;126 columnIndex: number;127 api: Grid.API<GridSpec>;128}) {129 const widths = api.xPositions$.useValue();130
131 const width = widths[columnIndex + 1] - widths[columnIndex];132 const measuredWidth = useMemo(() => {133 const m = measureText(value, api.viewport()!);134
135 return m?.width ?? 100;136 }, [api, value]);137
138 // Minus 6 as slight adjustment so we know there is definitely enough space139 if (measuredWidth > width - 6) return "#".repeat(Math.ceil(width / 16) + 1);140
141 return value;142}143
144export const makePerfHeaderCell = (name: string, subname: string) => {145 return (_: Grid.T.HeaderParams<GridSpec>) => {146 return (147 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">148 <div>{name}</div>149 <div className="text-ln-text-light font-mono uppercase">{subname}</div>150 </div>151 );152 };153};154
155export function SwitchToggle(props: { label: string; checked: boolean; onChange: (b: boolean) => void }) {156 const id = useId();157 return (158 <div className="flex items-center gap-2">159 <label className="text-ln-text-dark text-sm leading-none" htmlFor={id}>160 {props.label}161 </label>162 <Switch.Root163 className="bg-ln-gray-10 data-[state=checked]:bg-ln-primary-50 h-5.5 w-9.5 border-ln-border-strong relative cursor-pointer rounded-full border outline-none"164 id={id}165 checked={props.checked}166 onCheckedChange={(c) => props.onChange(c)}167 style={{ "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)" } as CSSProperties}168 >169 <Switch.Thumb className="size-4.5 block translate-x-0.5 rounded-full bg-white/95 shadow transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white" />170 </Switch.Root>171 </div>172 );173}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.
1function HashedValue({2 value,3 columnIndex,4 api,5}: {6 value: string;7 columnIndex: number;8 api: Grid.API<GridSpec>;9}) {10 const widths = api.xPositions$.useValue();11
12 const width = widths[columnIndex + 1] - widths[columnIndex];13 const measuredWidth = useMemo(() => {14 const m = measureText(value, api.viewport()!);15
16 return m.width;17 }, [api, value]);18
19 // Minus 6 as slight adjustment so we know there is definitely enough space20 if (measuredWidth > width - 6) return "#".repeat(Math.ceil(width / 16) + 1);21
22 return value;23}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: Learn how to specify default configuration options for columns.
- 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.
