Column Floating Header
LyteNyte Grid's floating row is a specialized header row that appears below the main column headers.
Floating header rows let you render custom components for each column. While often used for filtering components, it can render any type of component.
Enable the floating row by setting the
floatingRowEnabled property on the grid state.
Once enabled, each column can define a floating cell
renderer through the floatingCellRenderer property.
Set the floatingCellRenderer property on the column definition
to a React component that LyteNyte Grid can use to render
the column’s floating cell content.
Floating Component Renderer
When the floating row is enabled, LyteNyte Grid uses each column’s
floatingCellRenderer to decide what to render in the floating cell.
The demo below shows a simple renderer that provides a basic text filter input.
Column Floating Cell
39 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} from "./components.jsx";14import {15 useClientDataSource,16 Grid,17 type PieceWritable,18 usePiece,19} from "@1771technologies/lytenyte-pro";20import { useCallback, useMemo, useState } from "react";21
22const columns: Grid.Column<GridSpec>[] = [23 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },24 { id: "product", cellRenderer: ProductCell, width: 200, name: "Product", type: "string" },25 { id: "price", type: "number", cellRenderer: PriceCell, width: 100, name: "Price" },26 { id: "customer", cellRenderer: AvatarCell, width: 180, name: "Customer", type: "string" },27 { id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 130 },28 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },29 { id: "email", cellRenderer: EmailCell, width: 220, name: "Email", type: "string" },30];31
32export interface GridSpec {33 readonly data: OrderData;34 readonly api: {35 readonly filterModel: PieceWritable<Record<string, string | null>>;36 };37}38
39const base: Grid.ColumnBase<GridSpec> = { floatingCellRenderer: FloatingFilter };40
41export default function ColumnDemo() {42 const [filterModel, setFilterModel] = useState<Record<string, string | null>>({});43
44 const filter$ = usePiece(filterModel, setFilterModel);45 const apiExtension: GridSpec["api"] = useMemo(() => {46 return {47 filterModel: filter$,48 };49 }, [filter$]);50
51 const filterFunction: Grid.T.FilterFn<GridSpec["data"]> = useCallback(52 (row) => {53 const entries = Object.entries(filterModel);54 for (const [key, value] of entries) {55 const data = row.data[key as keyof OrderData];56 // The data should be defined, but if not removed this row in our filter57 if (!data) return false;58 if (!`${data}`.toLowerCase().includes(value?.toLowerCase() ?? "")) return false;59 }60 return true;61 },62 [filterModel],63 );64
65 const ds = useClientDataSource<GridSpec>({ data: data, filter: filterFunction });66
67 return (68 <div className={"ln-grid"} style={{ height: 500 }}>69 <Grid70 apiExtension={apiExtension}71 rowHeight={50}72 columns={columns}73 columnBase={base}74 rowSource={ds}75 floatingRowEnabled76 />77 </div>78 );79}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);135
136export function FloatingFilter({ column, api }: Grid.T.HeaderParams<GridSpec>) {137 const filters = api.filterModel.useValue();138 if (column.type !== "string") return "-";139
140 // Greatly simply the filter functionality for the demo purposes141 const filterForColumn = filters[column.id] || null;142
143 return (144 <input145 className={146 "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"147 }148 value={filterForColumn ?? ""}149 placeholder="Type to search..."150 onChange={(e) => {151 if (e.target.value === "") {152 api.filterModel.set((prev) => {153 const next = { ...prev };154 delete next[column.id];155 return next;156 });157 } else {158 api.filterModel.set((prev) => {159 const next = { ...prev };160 next[column.id] = e.target.value;161 return next;162 });163 }164 }}165 />166 );167}Floating Row Visibility Toggling
You can show or hide the floating row by toggling the
floatingRowEnabled state. The demo below includes a
switch that updates this state:
Floating Cell Visibility
40 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 SwitchToggle,14} from "./components.jsx";15import {16 useClientDataSource,17 Grid,18 type PieceWritable,19 usePiece,20} from "@1771technologies/lytenyte-pro";21import { useCallback, useMemo, useState } from "react";22
23const columns: Grid.Column<GridSpec>[] = [24 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },25 { id: "product", cellRenderer: ProductCell, width: 200, name: "Product", type: "string" },26 { id: "price", type: "number", cellRenderer: PriceCell, width: 100, name: "Price" },27 { id: "customer", cellRenderer: AvatarCell, width: 180, name: "Customer", type: "string" },28 { id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 130 },29 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },30 { id: "email", cellRenderer: EmailCell, width: 220, name: "Email", type: "string" },31];32
33export interface GridSpec {34 readonly data: OrderData;35 readonly api: {36 readonly filterModel: PieceWritable<Record<string, string | null>>;37 };38}39
40const base: Grid.ColumnBase<GridSpec> = { floatingCellRenderer: FloatingFilter };41
42export default function ColumnDemo() {43 const [filterModel, setFilterModel] = useState<Record<string, string | null>>({});44
45 const filter$ = usePiece(filterModel, setFilterModel);46 const apiExtension: GridSpec["api"] = useMemo(() => {47 return {48 filterModel: filter$,49 };50 }, [filter$]);51
52 const filterFunction: Grid.T.FilterFn<GridSpec["data"]> = useCallback(53 (row) => {54 const entries = Object.entries(filterModel);55 for (const [key, value] of entries) {56 const data = row.data[key as keyof OrderData];57 // The data should be defined, but if not removed this row in our filter58 if (!data) return false;59 if (!`${data}`.toLowerCase().includes(value?.toLowerCase() ?? "")) return false;60 }61 return true;62 },63 [filterModel],64 );65
66 const ds = useClientDataSource<GridSpec>({ data: data, filter: filterFunction });67
68 const [floatingRowEnabled, setFloatingRowEnabled] = useState(true);69
70 return (71 <>72 <div className="border-ln-border flex w-full border-b px-2 py-2">73 <SwitchToggle74 label="Toggle Floating Row"75 checked={floatingRowEnabled}76 onChange={() => {77 setFloatingRowEnabled((prev) => !prev);78 }}79 />80 </div>81
82 <div className={"ln-grid"} style={{ height: 500 }}>83 <Grid84 apiExtension={apiExtension}85 rowHeight={50}86 columns={columns}87 columnBase={base}88 rowSource={ds}89 floatingRowEnabled={floatingRowEnabled}90 />91 </div>92 </>93 );94}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 FloatingFilter({ column, api }: Grid.T.HeaderParams<GridSpec>) {138 const filters = api.filterModel.useValue();139 if (column.type !== "string") return "-";140
141 // Greatly simply the filter functionality for the demo purposes142 const filterForColumn = filters[column.id] || null;143
144 return (145 <input146 className={147 "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"148 }149 value={filterForColumn ?? ""}150 placeholder="Type to search..."151 onChange={(e) => {152 if (e.target.value === "") {153 api.filterModel.set((prev) => {154 const next = { ...prev };155 delete next[column.id];156 return next;157 });158 } else {159 api.filterModel.set((prev) => {160 const next = { ...prev };161 next[column.id] = e.target.value;162 return next;163 });164 }165 }}166 />167 );168}169
170export function SwitchToggle(props: { label: string; checked: boolean; onChange: (b: boolean) => void }) {171 const id = useId();172 return (173 <div className="flex items-center gap-2">174 <label className="text-ln-text-dark text-sm leading-none" htmlFor={id}>175 {props.label}176 </label>177 <Switch.Root178 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"179 id={id}180 checked={props.checked}181 onCheckedChange={(c) => props.onChange(c)}182 style={{ "-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)" } as CSSProperties}183 >184 <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" />185 </Switch.Root>186 </div>187 );188}The toggle simply flips the floatingRowEnabled value:
1<SwitchToggle2 label="Toggle Floating Row"3 checked={floatingRowEnabled}4 onChange={() => {5 setFloatingRowEnabled((prev) => !prev);6 }}7/>Next Steps
- Column Resizing: Change column widths programmatically or via user interaction.
- Column Moving: Reorder columns programmatically or through drag-and-drop.
- Column Base: Learn how to specify default configuration options for columns.
- Column Header Height: Set the height of the column header and column group header.
