Column Header Height
LyteNyte Grid lets you customize header height flexibly. The header's total height comes from the combined height of column group headers, column headers, and floating headers.
Use the headerHeight property on the grid to set the height of the header row in pixels.
The demo below showcases this feature, including programmatic updates.
Column Header Height
31 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 ToggleGroup,13 ToggleItem,14} from "./components.js";15import { useClientDataSource, Grid } from "@1771technologies/lytenyte-pro";16import { useState } from "react";17
18const columns: Grid.Column<GridSpec>[] = [19 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },20 { id: "product", cellRenderer: ProductCell, width: 200, name: "Product" },21 { id: "price", type: "number", cellRenderer: PriceCell, width: 100, name: "Price" },22 { id: "customer", cellRenderer: AvatarCell, width: 180, name: "Customer" },23 { id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 130 },24 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },25 { id: "email", cellRenderer: EmailCell, width: 220, name: "Email" },26];27
28export interface GridSpec {29 readonly data: OrderData;30}31
32
33export default function ColumnDemo() {34 const ds = useClientDataSource({ data: data });35
36 const [headerHeight, setHeaderHeight] = useState(40);37
38 return (39 <>40 <div className={"border-ln-border flex h-full items-center gap-1 text-nowrap border-b px-2 py-2"}>41 <div className={"text-light hidden text-xs font-medium md:block"}>Header Height:</div>42 <ToggleGroup43 type="single"44 value={`${headerHeight}`}45 className={"flex flex-wrap"}46 onValueChange={(c) => {47 if (!c) return;48 setHeaderHeight(Number.parseInt(c));49 }}50 >51 <ToggleItem value="30">Small</ToggleItem>52 <ToggleItem value="40">Medium</ToggleItem>53 <ToggleItem value="60">Large</ToggleItem>54 </ToggleGroup>55 </div>56 <div className={"ln-grid"} style={{ height: 500 }}>57 <Grid rowHeight={50} columns={columns} rowSource={ds} headerHeight={headerHeight} />58 </div>59 </>60 );61}1import { format } from "date-fns";2import { type JSX, type ReactNode } from "react";3import { ToggleGroup as TG } from "radix-ui";4import type { Grid } from "@1771technologies/lytenyte-pro";5import type { GridSpec } from "./demo.jsx";6import { clsx, type ClassValue } from "clsx";7import { twMerge } from "tailwind-merge";8
9function tw(...c: ClassValue[]) {10 return twMerge(clsx(...c));11}12
13export function ProductCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return;15
16 const url = row.data?.productThumbnail;17 const title = row.data.product;18 const desc = row.data.productDescription;19
20 return (21 <div className="flex h-full w-full items-center gap-2">22 <img className="border-ln-border-strong h-7 w-7 rounded-lg border" src={url} alt={title + desc} />23 <div className="text-ln-text-dark flex flex-col gap-0.5">24 <div className="font-semibold">{title}</div>25 <div className="text-ln-text-light text-xs">{desc}</div>26 </div>27 </div>28 );29}30
31export function AvatarCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {32 if (!api.rowIsLeaf(row) || !row.data) return;33
34 const url = row.data?.customerAvatar;35
36 const name = row.data.customer;37
38 return (39 <div className="flex h-full w-full items-center gap-2">40 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />41 <div className="text-ln-text-dark flex flex-col gap-0.5">42 <div>{name}</div>43 </div>44 </div>45 );46}47
48const formatter = new Intl.NumberFormat("en-Us", {49 minimumFractionDigits: 2,50 maximumFractionDigits: 2,51});52export function PriceCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {53 if (!api.rowIsLeaf(row) || !row.data) return;54
55 const price = formatter.format(row.data.price);56 const [dollars, cents] = price.split(".");57
58 return (59 <div className="flex h-full w-full items-center justify-end">60 <div className="flex items-baseline tabular-nums">61 <span className="text-ln-text font-semibold">${dollars}</span>.62 <span className="relative text-xs">{cents}</span>63 </div>64 </div>65 );66}67
68export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {69 if (!api.rowIsLeaf(row) || !row.data) return;70
71 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");72
73 return <div className="flex h-full w-full items-center">{formattedDate}</div>;74}75export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {76 if (!api.rowIsLeaf(row) || !row.data) return;77
78 return <div className="text-xs tabular-nums">{row.data.id}</div>;79}80
81export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {82 if (!api.rowIsLeaf(row) || !row.data) return;83
84 const cardNumber = row.data.cardNumber;85 const provider = row.data.paymentMethod;86
87 let Logo: ReactNode = null;88 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;89 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;90
91 return (92 <div className="flex h-full w-full items-center gap-2">93 <div className="flex w-7 items-center justify-center">{Logo}</div>94 <div className="flex items-center gap-px">95 <div className="bg-ln-gray-40 size-2 rounded-full"></div>96 <div className="bg-ln-gray-40 size-2 rounded-full"></div>97 <div className="bg-ln-gray-40 size-2 rounded-full"></div>98 <div className="bg-ln-gray-40 size-2 rounded-full"></div>99 </div>100 <div className="tabular-nums">{cardNumber}</div>101 </div>102 );103}104
105export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {106 if (!api.rowIsLeaf(row) || !row.data) return;107
108 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;109}110
111const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (112 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>113 <path114 fill="#1434cb"115 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"116 />117 </svg>118);119
120const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (121 <svg122 xmlns="http://www.w3.org/2000/svg"123 width={2500}124 height={1524}125 viewBox="55.2 38.3 464.5 287.8"126 {...props}127 >128 <path129 fill="#f79f1a"130 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"131 />132 <path133 fill="#ea001b"134 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"135 />136 <path137 fill="#ff5f01"138 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"139 />140 </svg>141);142
143export function ToggleGroup(props: Parameters<typeof TG.Root>[0]) {144 return (145 <TG.Root146 {...props}147 className={tw("bg-ln-gray-20 flex items-center gap-2 rounded-xl px-2 py-1", props.className)}148 ></TG.Root>149 );150}151
152export function ToggleItem(props: Parameters<typeof TG.Item>[0]) {153 return (154 <TG.Item155 {...props}156 className={tw(157 "text-ln-text flex cursor-pointer items-center justify-center px-2 py-1 text-xs font-bold outline-none focus:outline-none",158 "data-[state=on]:text-ln-text-dark data-[state=on]:bg-linear-to-b from-ln-gray-02 to-ln-gray-05 data-[state=on]:rounded-md",159 props.className,160 )}161 ></TG.Item>162 );163}Column Groups & Header Spanning
When column groups are enabled, some column headers may span across multiple group rows, increasing the cell’s header height since it must bridge the missing group rows. This occurs when:
- A column does not belong to any group
- The columns group path is shallower than the deepest group level
In the demo below, Price and Product belong to the Inventory group. All other columns are ungrouped, so their header cells span every group row and reach the leaf row, giving them the full header height. A header cell’s height is the leaf header (base) height plus the combined height of the group rows it spans.
If all group rows share height G, then header height is determined as follows:
1finalHeaderHeight = baseHeaderHeight + (spannedGroupRowCount * G)Column Header Group Span
38 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 ToggleGroup,13 ToggleItem,14} from "./components.jsx";15import { useClientDataSource, Grid } from "@1771technologies/lytenyte-pro";16import { useState } from "react";17
18export interface GridSpec {19 readonly data: OrderData;20}21
22const columns: Grid.Column<GridSpec>[] = [23 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },24 { id: "product", name: "Product", cellRenderer: ProductCell, groupPath: ["Inventory"], width: 200 },25 {26 id: "price",27 name: "Price",28 type: "number",29 cellRenderer: PriceCell,30 groupPath: ["Inventory"],31 width: 100,32 },33 { id: "customer", name: "Customer", cellRenderer: AvatarCell, width: 180 },34 { id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 120 },35 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },36 { id: "email", name: "Email", cellRenderer: EmailCell, width: 220 },37];38
39
40export default function ColumnDemo() {41 const ds = useClientDataSource({ data: data });42
43 const [headerHeight, setHeaderHeight] = useState(40);44
45 return (46 <>47 <div className={"border-ln-border flex h-full items-center gap-1 text-nowrap border-b px-2 py-2"}>48 <div className={"text-light hidden text-xs font-medium md:block"}>Header Height:</div>49 <ToggleGroup50 type="single"51 value={`${headerHeight}`}52 className={"flex flex-wrap"}53 onValueChange={(c) => {54 if (!c) return;55 setHeaderHeight(Number.parseInt(c));56 }}57 >58 <ToggleItem value="30">Small</ToggleItem>59 <ToggleItem value="40">Medium</ToggleItem>60 <ToggleItem value="60">Large</ToggleItem>61 </ToggleGroup>62 </div>63 <div className={"ln-grid ln-header-group:justify-center"} style={{ height: 500 }}>64 <Grid rowHeight={50} columns={columns} rowSource={ds} headerHeight={headerHeight} />65 </div>66 </>67 );68}1import { format } from "date-fns";2import { type JSX, type ReactNode } from "react";3import { ToggleGroup as TG } from "radix-ui";4import type { Grid } from "@1771technologies/lytenyte-pro";5import type { GridSpec } from "./demo.jsx";6import { clsx, type ClassValue } from "clsx";7import { twMerge } from "tailwind-merge";8
9function tw(...c: ClassValue[]) {10 return twMerge(clsx(...c));11}12
13export function ProductCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return;15
16 const url = row.data?.productThumbnail;17 const title = row.data.product;18 const desc = row.data.productDescription;19
20 return (21 <div className="flex h-full w-full items-center gap-2">22 <img className="border-ln-border-strong h-7 w-7 rounded-lg border" src={url} alt={title + desc} />23 <div className="text-ln-text-dark flex flex-col gap-0.5">24 <div className="font-semibold">{title}</div>25 <div className="text-ln-text-light text-xs">{desc}</div>26 </div>27 </div>28 );29}30
31export function AvatarCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {32 if (!api.rowIsLeaf(row) || !row.data) return;33
34 const url = row.data?.customerAvatar;35
36 const name = row.data.customer;37
38 return (39 <div className="flex h-full w-full items-center gap-2">40 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />41 <div className="text-ln-text-dark flex flex-col gap-0.5">42 <div>{name}</div>43 </div>44 </div>45 );46}47
48const formatter = new Intl.NumberFormat("en-Us", {49 minimumFractionDigits: 2,50 maximumFractionDigits: 2,51});52export function PriceCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {53 if (!api.rowIsLeaf(row) || !row.data) return;54
55 const price = formatter.format(row.data.price);56 const [dollars, cents] = price.split(".");57
58 return (59 <div className="flex h-full w-full items-center justify-end">60 <div className="flex items-baseline tabular-nums">61 <span className="text-ln-text font-semibold">${dollars}</span>.62 <span className="relative text-xs">{cents}</span>63 </div>64 </div>65 );66}67
68export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {69 if (!api.rowIsLeaf(row) || !row.data) return;70
71 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");72
73 return <div className="flex h-full w-full items-center">{formattedDate}</div>;74}75export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {76 if (!api.rowIsLeaf(row) || !row.data) return;77
78 return <div className="text-xs tabular-nums">{row.data.id}</div>;79}80
81export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {82 if (!api.rowIsLeaf(row) || !row.data) return;83
84 const cardNumber = row.data.cardNumber;85 const provider = row.data.paymentMethod;86
87 let Logo: ReactNode = null;88 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;89 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;90
91 return (92 <div className="flex h-full w-full items-center gap-2">93 <div className="flex w-7 items-center justify-center">{Logo}</div>94 <div className="flex items-center gap-px">95 <div className="bg-ln-gray-40 size-2 rounded-full"></div>96 <div className="bg-ln-gray-40 size-2 rounded-full"></div>97 <div className="bg-ln-gray-40 size-2 rounded-full"></div>98 <div className="bg-ln-gray-40 size-2 rounded-full"></div>99 </div>100 <div className="tabular-nums">{cardNumber}</div>101 </div>102 );103}104
105export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {106 if (!api.rowIsLeaf(row) || !row.data) return;107
108 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;109}110
111const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (112 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>113 <path114 fill="#1434cb"115 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"116 />117 </svg>118);119
120const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (121 <svg122 xmlns="http://www.w3.org/2000/svg"123 width={2500}124 height={1524}125 viewBox="55.2 38.3 464.5 287.8"126 {...props}127 >128 <path129 fill="#f79f1a"130 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"131 />132 <path133 fill="#ea001b"134 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"135 />136 <path137 fill="#ff5f01"138 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"139 />140 </svg>141);142
143export function ToggleGroup(props: Parameters<typeof TG.Root>[0]) {144 return (145 <TG.Root146 {...props}147 className={tw("bg-ln-gray-20 flex items-center gap-2 rounded-xl px-2 py-1", props.className)}148 ></TG.Root>149 );150}151
152export function ToggleItem(props: Parameters<typeof TG.Item>[0]) {153 return (154 <TG.Item155 {...props}156 className={tw(157 "text-ln-text flex cursor-pointer items-center justify-center px-2 py-1 text-xs font-bold outline-none focus:outline-none",158 "data-[state=on]:text-ln-text-dark data-[state=on]:bg-linear-to-b from-ln-gray-02 to-ln-gray-05 data-[state=on]:rounded-md",159 props.className,160 )}161 ></TG.Item>162 );163}Column Group Header Height
Set the height of group header rows using the headerGroupHeight property on the grid.
All column groups share the same group-header height.
Column Group Header Height
50 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 ToggleGroup,13 ToggleItem,14} from "./components.jsx";15import { useClientDataSource, Grid } from "@1771technologies/lytenyte-pro";16import { useState } from "react";17
18export interface GridSpec {19 readonly data: OrderData;20}21
22const columns: Grid.Column<GridSpec>[] = [23 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },24 { id: "product", name: "Product", cellRenderer: ProductCell, groupPath: ["Inventory"], width: 200 },25 {26 id: "price",27 name: "Price",28 type: "number",29 cellRenderer: PriceCell,30 groupPath: ["Inventory"],31 width: 100,32 },33 { id: "customer", name: "Customer", cellRenderer: AvatarCell, width: 180, groupPath: ["Details"] },34 {35 id: "purchaseDate",36 cellRenderer: PurchaseDateCell,37 name: "Purchase Date",38 width: 120,39 groupPath: ["Details"],40 },41 {42 id: "paymentMethod",43 cellRenderer: PaymentMethodCell,44 name: "Payment Method",45 width: 150,46 groupPath: ["Details"],47 },48 { id: "email", name: "Email", cellRenderer: EmailCell, width: 220, groupPath: ["Details"] },49];50
51
52export default function ColumnDemo() {53 const ds = useClientDataSource({ data: data });54
55 const [headerGroupHeight, setHeaderGroupHeight] = useState(40);56
57 return (58 <>59 <div className={"border-ln-border flex h-full items-center gap-1 text-nowrap border-b px-2 py-2"}>60 <div className={"text-light hidden text-xs font-medium md:block"}>Header Group Height:</div>61 <ToggleGroup62 type="single"63 value={`${headerGroupHeight}`}64 className={"flex flex-wrap"}65 onValueChange={(c) => {66 if (!c) return;67 setHeaderGroupHeight(Number.parseInt(c));68 }}69 >70 <ToggleItem value="30">Small</ToggleItem>71 <ToggleItem value="40">Medium</ToggleItem>72 <ToggleItem value="60">Large</ToggleItem>73 </ToggleGroup>74 </div>75 <div className={"ln-grid ln-header-group:justify-center"} style={{ height: 500 }}>76 <Grid77 rowHeight={50}78 rowSource={ds}79 columns={columns}80 headerGroupHeight={headerGroupHeight}81 />82 </div>83 </>84 );85}1import { format } from "date-fns";2import { type JSX, type ReactNode } from "react";3import { ToggleGroup as TG } from "radix-ui";4import type { Grid } from "@1771technologies/lytenyte-pro";5import type { GridSpec } from "./demo.jsx";6import { clsx, type ClassValue } from "clsx";7import { twMerge } from "tailwind-merge";8
9function tw(...c: ClassValue[]) {10 return twMerge(clsx(...c));11}12
13export function ProductCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return;15
16 const url = row.data?.productThumbnail;17 const title = row.data.product;18 const desc = row.data.productDescription;19
20 return (21 <div className="flex h-full w-full items-center gap-2">22 <img className="border-ln-border-strong h-7 w-7 rounded-lg border" src={url} alt={title + desc} />23 <div className="text-ln-text-dark flex flex-col gap-0.5">24 <div className="font-semibold">{title}</div>25 <div className="text-ln-text-light text-xs">{desc}</div>26 </div>27 </div>28 );29}30
31export function AvatarCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {32 if (!api.rowIsLeaf(row) || !row.data) return;33
34 const url = row.data?.customerAvatar;35
36 const name = row.data.customer;37
38 return (39 <div className="flex h-full w-full items-center gap-2">40 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />41 <div className="text-ln-text-dark flex flex-col gap-0.5">42 <div>{name}</div>43 </div>44 </div>45 );46}47
48const formatter = new Intl.NumberFormat("en-Us", {49 minimumFractionDigits: 2,50 maximumFractionDigits: 2,51});52export function PriceCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {53 if (!api.rowIsLeaf(row) || !row.data) return;54
55 const price = formatter.format(row.data.price);56 const [dollars, cents] = price.split(".");57
58 return (59 <div className="flex h-full w-full items-center justify-end">60 <div className="flex items-baseline tabular-nums">61 <span className="text-ln-text font-semibold">${dollars}</span>.62 <span className="relative text-xs">{cents}</span>63 </div>64 </div>65 );66}67
68export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {69 if (!api.rowIsLeaf(row) || !row.data) return;70
71 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");72
73 return <div className="flex h-full w-full items-center">{formattedDate}</div>;74}75export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {76 if (!api.rowIsLeaf(row) || !row.data) return;77
78 return <div className="text-xs tabular-nums">{row.data.id}</div>;79}80
81export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {82 if (!api.rowIsLeaf(row) || !row.data) return;83
84 const cardNumber = row.data.cardNumber;85 const provider = row.data.paymentMethod;86
87 let Logo: ReactNode = null;88 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;89 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;90
91 return (92 <div className="flex h-full w-full items-center gap-2">93 <div className="flex w-7 items-center justify-center">{Logo}</div>94 <div className="flex items-center gap-px">95 <div className="bg-ln-gray-40 size-2 rounded-full"></div>96 <div className="bg-ln-gray-40 size-2 rounded-full"></div>97 <div className="bg-ln-gray-40 size-2 rounded-full"></div>98 <div className="bg-ln-gray-40 size-2 rounded-full"></div>99 </div>100 <div className="tabular-nums">{cardNumber}</div>101 </div>102 );103}104
105export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {106 if (!api.rowIsLeaf(row) || !row.data) return;107
108 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;109}110
111const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (112 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>113 <path114 fill="#1434cb"115 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"116 />117 </svg>118);119
120const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (121 <svg122 xmlns="http://www.w3.org/2000/svg"123 width={2500}124 height={1524}125 viewBox="55.2 38.3 464.5 287.8"126 {...props}127 >128 <path129 fill="#f79f1a"130 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"131 />132 <path133 fill="#ea001b"134 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"135 />136 <path137 fill="#ff5f01"138 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"139 />140 </svg>141);142
143export function ToggleGroup(props: Parameters<typeof TG.Root>[0]) {144 return (145 <TG.Root146 {...props}147 className={tw("bg-ln-gray-20 flex items-center gap-2 rounded-xl px-2 py-1", props.className)}148 ></TG.Root>149 );150}151
152export function ToggleItem(props: Parameters<typeof TG.Item>[0]) {153 return (154 <TG.Item155 {...props}156 className={tw(157 "text-ln-text flex cursor-pointer items-center justify-center px-2 py-1 text-xs font-bold outline-none focus:outline-none",158 "data-[state=on]:text-ln-text-dark data-[state=on]:bg-linear-to-b from-ln-gray-02 to-ln-gray-05 data-[state=on]:rounded-md",159 props.className,160 )}161 ></TG.Item>162 );163}Floating Row Height
The floating row appears directly under the column header and is useful for supplemental UI,
such as floating filters. Set its height using the floatingRowHeight property.
The demo below shows a simple example that adds text-matching filters for string columns. For more details, see the Column Floating Header guide.
Floating Row Height
43 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 FloatingFilter,8 IdCell,9 PaymentMethodCell,10 PriceCell,11 ProductCell,12 PurchaseDateCell,13 ToggleGroup,14 ToggleItem,15} from "./components.jsx";16import {17 useClientDataSource,18 Grid,19 type PieceWritable,20 usePiece,21 type Piece,22} from "@1771technologies/lytenyte-pro";23import { useCallback, useMemo, useState } from "react";24
25const columns: Grid.Column<GridSpec>[] = [26 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },27 { id: "product", cellRenderer: ProductCell, width: 200, name: "Product", type: "string" },28 { id: "price", type: "number", cellRenderer: PriceCell, width: 100, name: "Price" },29 { id: "customer", cellRenderer: AvatarCell, width: 180, name: "Customer", type: "string" },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", type: "string" },33];34
35export interface GridSpec {36 readonly data: OrderData;37 readonly api: {38 readonly filterModel: PieceWritable<Record<string, string | null>>;39 readonly isSmallFilter: Piece<boolean>;40 };41}42
43const base: Grid.ColumnBase<GridSpec> = { floatingCellRenderer: FloatingFilter };44
45export default function ColumnDemo() {46 const [filterModel, setFilterModel] = useState<Record<string, string | null>>({});47 const [floatingRowHeight, setFloatingRowHeight] = useState(40);48
49 const filter$ = usePiece(filterModel, setFilterModel);50 const small$ = usePiece(floatingRowHeight < 40);51 const apiExtension: GridSpec["api"] = useMemo(() => {52 return {53 filterModel: filter$,54 isSmallFilter: small$,55 };56 }, [filter$, small$]);57
58 const filterFunction: Grid.T.FilterFn<GridSpec["data"]> = useCallback(59 (row) => {60 const entries = Object.entries(filterModel);61 for (const [key, value] of entries) {62 const data = row.data[key as keyof OrderData];63 // The data should be defined, but if not removed this row in our filter64 if (!data) return false;65 if (!`${data}`.toLowerCase().includes(value?.toLowerCase() ?? "")) return false;66 }67 return true;68 },69 [filterModel],70 );71
72 const ds = useClientDataSource<GridSpec>({ data: data, filter: filterFunction });73
74 return (75 <>76 <div className={"border-ln-border flex h-full items-center gap-1 text-nowrap border-b px-2 py-2"}>77 <div className={"text-light hidden text-xs font-medium md:block"}>Floating Row Height</div>78 <ToggleGroup79 type="single"80 value={`${floatingRowHeight}`}81 className={"flex flex-wrap"}82 onValueChange={(c) => {83 if (!c) return;84 setFloatingRowHeight(Number.parseInt(c));85 }}86 >87 <ToggleItem value="30">Small</ToggleItem>88 <ToggleItem value="40">Medium</ToggleItem>89 <ToggleItem value="60">Large</ToggleItem>90 </ToggleGroup>91 </div>92 <div className={"ln-grid"} style={{ height: 500 }}>93 <Grid94 apiExtension={apiExtension}95 rowHeight={50}96 columns={columns}97 columnBase={base}98 rowSource={ds}99 floatingRowEnabled100 floatingRowHeight={floatingRowHeight}101 />102 </div>103 </>104 );105}1import { format } from "date-fns";2import { type JSX, type ReactNode } from "react";3import { ToggleGroup as TG } from "radix-ui";4import type { Grid } from "@1771technologies/lytenyte-pro";5import type { GridSpec } from "./demo.jsx";6import { clsx, type ClassValue } from "clsx";7import { twMerge } from "tailwind-merge";8
9function tw(...c: ClassValue[]) {10 return twMerge(clsx(...c));11}12
13export function ProductCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return;15
16 const url = row.data?.productThumbnail;17 const title = row.data.product;18 const desc = row.data.productDescription;19
20 return (21 <div className="flex h-full w-full items-center gap-2">22 <img className="border-ln-border-strong h-7 w-7 rounded-lg border" src={url} alt={title + desc} />23 <div className="text-ln-text-dark flex flex-col gap-0.5">24 <div className="font-semibold">{title}</div>25 <div className="text-ln-text-light text-xs">{desc}</div>26 </div>27 </div>28 );29}30
31export function AvatarCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {32 if (!api.rowIsLeaf(row) || !row.data) return;33
34 const url = row.data?.customerAvatar;35
36 const name = row.data.customer;37
38 return (39 <div className="flex h-full w-full items-center gap-2">40 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />41 <div className="text-ln-text-dark flex flex-col gap-0.5">42 <div>{name}</div>43 </div>44 </div>45 );46}47
48const formatter = new Intl.NumberFormat("en-Us", {49 minimumFractionDigits: 2,50 maximumFractionDigits: 2,51});52export function PriceCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {53 if (!api.rowIsLeaf(row) || !row.data) return;54
55 const price = formatter.format(row.data.price);56 const [dollars, cents] = price.split(".");57
58 return (59 <div className="flex h-full w-full items-center justify-end">60 <div className="flex items-baseline tabular-nums">61 <span className="text-ln-text font-semibold">${dollars}</span>.62 <span className="relative text-xs">{cents}</span>63 </div>64 </div>65 );66}67
68export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {69 if (!api.rowIsLeaf(row) || !row.data) return;70
71 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");72
73 return <div className="flex h-full w-full items-center">{formattedDate}</div>;74}75export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {76 if (!api.rowIsLeaf(row) || !row.data) return;77
78 return <div className="text-xs tabular-nums">{row.data.id}</div>;79}80
81export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {82 if (!api.rowIsLeaf(row) || !row.data) return;83
84 const cardNumber = row.data.cardNumber;85 const provider = row.data.paymentMethod;86
87 let Logo: ReactNode = null;88 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;89 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;90
91 return (92 <div className="flex h-full w-full items-center gap-2">93 <div className="flex w-7 items-center justify-center">{Logo}</div>94 <div className="flex items-center gap-px">95 <div className="bg-ln-gray-40 size-2 rounded-full"></div>96 <div className="bg-ln-gray-40 size-2 rounded-full"></div>97 <div className="bg-ln-gray-40 size-2 rounded-full"></div>98 <div className="bg-ln-gray-40 size-2 rounded-full"></div>99 </div>100 <div className="tabular-nums">{cardNumber}</div>101 </div>102 );103}104
105export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {106 if (!api.rowIsLeaf(row) || !row.data) return;107
108 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;109}110
111const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (112 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>113 <path114 fill="#1434cb"115 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"116 />117 </svg>118);119
120const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (121 <svg122 xmlns="http://www.w3.org/2000/svg"123 width={2500}124 height={1524}125 viewBox="55.2 38.3 464.5 287.8"126 {...props}127 >128 <path129 fill="#f79f1a"130 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"131 />132 <path133 fill="#ea001b"134 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"135 />136 <path137 fill="#ff5f01"138 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"139 />140 </svg>141);142
143export function ToggleGroup(props: Parameters<typeof TG.Root>[0]) {144 return (145 <TG.Root146 {...props}147 className={tw("bg-ln-gray-20 flex items-center gap-2 rounded-xl px-2 py-1", props.className)}148 ></TG.Root>149 );150}151
152export function ToggleItem(props: Parameters<typeof TG.Item>[0]) {153 return (154 <TG.Item155 {...props}156 className={tw(157 "text-ln-text flex cursor-pointer items-center justify-center px-2 py-1 text-xs font-bold outline-none focus:outline-none",158 "data-[state=on]:text-ln-text-dark data-[state=on]:bg-linear-to-b from-ln-gray-02 to-ln-gray-05 data-[state=on]:rounded-md",159 props.className,160 )}161 ></TG.Item>162 );163}164
165export function FloatingFilter({ column, api }: Grid.T.HeaderParams<GridSpec>) {166 const filters = api.filterModel.useValue();167 if (column.type !== "string") return "-";168
169 // Greatly simply the filter functionality for the demo purposes170 const filterForColumn = filters[column.id] || null;171
172 const isSmall = api.isSmallFilter.useValue();173
174 return (175 <input176 className={tw(177 "border-ln-gray-30 focus:outline-ln-primary-50 h-[calc(100%-8px)] w-full rounded-lg border px-2 text-sm focus:outline-1",178 isSmall && "rounded text-xs",179 )}180 value={filterForColumn ?? ""}181 placeholder="Type to search..."182 onChange={(e) => {183 if (e.target.value === "") {184 api.filterModel.set((prev) => {185 const next = { ...prev };186 delete next[column.id];187 return next;188 });189 } else {190 api.filterModel.set((prev) => {191 const next = { ...prev };192 next[column.id] = e.target.value;193 return next;194 });195 }196 }}197 />198 );199}Next Steps
- Column Resizing: Change column widths programmatically or via user interaction.
- Column Floating Header: Add a secondary header row for extra header features.
- Column Moving: Reorder columns programmatically or through drag-and-drop.
- Column Field: Control how a column retrieves its value for each cell.
