Column Visibility
Learn how to manage column visibility in LyteNyte Grid and understand the difference between hidden columns and collapsed column groups.
LyteNyte Grid supports defining columns that are initially hidden. Developers can toggle column visibility during runtime. The grid also supports column groups, allowing related columns to be shown or hidden together.
Hiding Columns
Each column in LyteNyte Grid has a hide property in its specification. This property defaults to false.
When set to true, the grid does not render the column in the viewport.
The grid still includes the column internally, and queries against
grid state will continue to return it.
In the demo below, the Network and Exchange columns start hidden. Clicking their pills toggles each column's visibility. When a hidden column becomes visible, the grid inserts it based on its position in the column state.
Hiding Columns
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId } from "react";import {ExchangeCell,makePerfHeaderCell,NetworkCell,PercentCell,PercentCellPositiveNegative,SymbolCell,tw,} from "./components";import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";import { ColumnPills } from "./pill-manager";const columns: Column<DEXPerformanceData>[] = [{ id: "symbol", cellRenderer: SymbolCell, width: 220, name: "Symbol" },{ id: "network", cellRenderer: NetworkCell, width: 220, hide: true, name: "Network" },{ id: "exchange", cellRenderer: ExchangeCell, width: 220, hide: true, name: "Exchange" },{id: "change24h",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Change", "24h"),name: "Change % 24h",type: "number,",},{id: "perf1w",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "1w"),name: "Perf % 1W",type: "number,",},{id: "perf1m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "1m"),name: "Perf % 1M",type: "number,",},{id: "perf3m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "3m"),name: "Perf % 3M",type: "number,",},{id: "perf6m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "6m"),name: "Perf % 6M",type: "number,",},{id: "perfYtd",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "YTD"),name: "Perf % YTD",type: "number",},{ id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },{id: "volatility1m",cellRenderer: PercentCell,headerRenderer: makePerfHeaderCell("Volatility", "1m"),name: "Volatility 1M",type: "number",},];export default function ColumnBase() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 80 },});const view = grid.view.useValue();return (<div><ColumnPills grid={grid} /><div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("text-ln-gray-60! dark:text-ln-gray-70! flex h-full w-full items-center text-nowrap px-2 text-xs capitalize",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className="text-xs! flex h-full w-full items-center px-2"/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div></div>);}
import "./component.css";import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";import {exchanges,networks,symbols,} from "@1771technologies/grid-sample-data/dex-pairs-performance";export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function SymbolCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const ticker = row.data.symbolTicker;const symbol = row.data.symbol;const image = symbols[row.data.symbolTicker];return (<div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for symbol ${symbol}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="bg-ln-gray-20 text-ln-gray-100 flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">{ticker}</div><div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div></div>);}export function NetworkCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const name = row.data.network;const image = networks[name];return (<div className="grid grid-cols-[20px_1fr] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for network ${name}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="w-full overflow-hidden text-ellipsis">{name}</div></div>);}export function ExchangeCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const name = row.data.exchange;const image = exchanges[name];return (<div className="grid grid-cols-[20px_1fr] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for exchange ${name}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="w-full overflow-hidden text-ellipsis">{name}</div></div>);}export function PercentCellPositiveNegative({grid: { api },column,row,}: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const field = api.columnField(column, row);if (typeof field !== "number") return "-";const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";return (<divclassName={tw("h-ful flex w-full items-center justify-end tabular-nums",field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",)}>{value}</div>);}export function PercentCell({grid: { api },column,row,}: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const field = api.columnField(column, row);if (typeof field !== "number") return "-";const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;}export const makePerfHeaderCell = (name: string, subname: string) => {return (_: HeaderCellRendererParams<DEXPerformanceData>) => {return (<div className="flex h-full w-full flex-col items-end justify-center"><div>{name}</div><div className="font-mono uppercase">{subname}</div></div>);};};
import { GridBox } from "@1771technologies/lytenyte-pro";import type { JSX } from "react";import { useState, type PropsWithChildren, type ReactNode } from "react";import {CollapseGroupIcon,ColumnsIcon,DragDotsSmallIcon,ExpandGroupIcon,} from "@1771technologies/lytenyte-pro/icons";import type { Grid } from "@1771technologies/lytenyte-pro/types";import { tw } from "./components";export function ColumnPills<T>({ grid }: { grid: Grid<T> }) {const { rootProps, items } = GridBox.useColumnBoxItems({grid,draggable: true,orientation: "horizontal",placeholder: (el) => el.firstElementChild! as HTMLElement,onDrop: (p) => {if (p.src.id === p.target.id) return;grid.api.columnMove({moveColumns: [p.src],moveTarget: p.target,before: p.isBefore,});},});return (<GridBox.Root {...rootProps}><PillManagerRow icon={<ColumnsIcon />} label="Columns">{items.map((c) => {return (<GridBox.Itemkey={c.id}item={c}className={tw("flex h-[52px] items-center", "horizontal-indicators")}onKeyDown={(e) => {if (e.key === " " &&((e.target as HTMLElement).parentElement as HTMLElement) === e.currentTarget) {grid.api.columnUpdate({[c.data.id]: { hide: !c.data.hide },});}}}itemClassName={tw("h-full flex items-center px-[6px] focus:outline-none group text-ln-gray-90 ","opacity-60 hover:opacity-80 transition-opacity",!c.data.hide && "opacity-100 hover:opacity-100",)}><divonClick={(e) => {if (e.currentTarget.contains(e.target as HTMLElement))grid.api.columnUpdate({[c.data.id]: { hide: !c.data.hide },});}}className="bg-ln-pill-column-fill border-ln-pill-column-stroke group-focus-visible:ring-ln-primary-50 flex h-7 cursor-pointer items-center text-nowrap rounded border pl-1 group-focus-visible:ring-1"><DragDotsSmallIcon className="no-drag cursor-grab" /><div className="pl-1 pr-3 text-xs">{c.label}</div></div></GridBox.Item>);})}</PillManagerRow></GridBox.Root>);}export interface PillManagerRowProps {readonly icon: ReactNode;readonly label: string;readonly className?: string;}export function PillManagerRow({icon,label,children,className,}: PropsWithChildren<PillManagerRowProps>) {const [expanded, setExpanded] = useState(false);return (<divclassName={tw("bg-ln-gray-05 border-ln-gray-20 grid grid-cols-[42px_1fr_64px] border-t md:grid-cols-[151px_1fr_40px]",)}><div className="text-ln-gray-80 flex min-h-[52px] items-center justify-center gap-2 text-sm md:justify-start md:pl-[30px] md:pr-3">{icon}<div className="hidden md:block">{label}</div></div><GridBox.PanelclassName={tw("no-scrollbar flex max-h-[200px] w-full items-center overflow-auto focus:outline-none md:max-h-[unset]","focus-visible:outline-ln-primary-50 focus-visible:outline focus-visible:-outline-offset-1",expanded && "flex-wrap",className,)}>{children}</GridBox.Panel><divclassName={tw("border-ln-gray-30 relative flex items-center justify-center gap-1 border-l","before:bg-linear-to-tr before:absolute before:-left-1 before:h-full before:w-1 before:from-transparent before:to-[rgba(0,0,0,0.075)]",)}><GridIconButton onClick={() => setExpanded((prev) => !prev)} className="h-7 min-w-7">{expanded ? (<CollapseGroupIcon width={20} height={20} />) : (<ExpandGroupIcon width={20} height={20} />)}</GridIconButton></div></div>);}export function GridIconButton(props: JSX.IntrinsicElements["button"]) {return (<button{...props}className={tw("hover:bg-ln-gray-20 text-ln-gray-70 focus-visible:outline-ln-primary-50 flex size-7 min-h-6 min-w-6 cursor-pointer items-center justify-center rounded transition-colors focus:outline-none focus-visible:outline-1 focus-visible:outline-offset-[-3px]",props.className,)}></button>);}
The Pill Manager calls the grid API's columnUpdate method when a pill is clicked.
The snippet below shows how the handler toggles the hide property. LyteNyte Grid handles
updating the visual display of the viewport.
grid.api.columnUpdate({[c.data.id]: { hide: !c.data.hide },});
Avoid updating a column's visibility too frequently. Each update triggers a layout recalculation and DOM update, which can be expensive when performed many times per second.
Column Group Visibility
A column group is a developer-defined collection of related columns. A group may be collapsed, causing certain columns to appear only when the group is open.
When the grid collapses a group, it hides the affected columns visually.
However, this behavior differs from setting hide on a column specification.
The grid still considers these columns logically visible.
In other words, their visibility state remains true even though the
collapsed group hides them in the UI.
The example below demonstrates this behavior. The Market Info group is collapsed, which hides the Network and Exchange columns. The Pill Manager still shows them as visible because the grid treats them as visible in state, even though the collapsed group hides them visually.
Column Group Visibility
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId } from "react";import {ExchangeCell,makePerfHeaderCell,NetworkCell,PercentCell,PercentCellPositiveNegative,SymbolCell,tw,} from "./components";import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";import { ColumnPills } from "./pill-manager";import { ChevronLeftIcon, ChevronRightIcon } from "@1771technologies/lytenyte-pro/icons";const columns: Column<DEXPerformanceData>[] = [{id: "symbol",cellRenderer: SymbolCell,width: 220,name: "Symbol",groupPath: ["Market Info"],groupVisibility: "always",},{id: "network",cellRenderer: NetworkCell,width: 220,name: "Network",groupPath: ["Market Info"],groupVisibility: "open",},{id: "exchange",cellRenderer: ExchangeCell,width: 220,name: "Exchange",groupPath: ["Market Info"],groupVisibility: "open",},{id: "change24h",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Change", "24h"),name: "Change % 24h",type: "number,",},{id: "perf1w",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "1w"),name: "Perf % 1W",type: "number,",},{id: "perf1m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "1m"),name: "Perf % 1M",type: "number,",},{id: "perf3m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "3m"),name: "Perf % 3M",type: "number,",},{id: "perf6m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "6m"),name: "Perf % 6M",type: "number,",},{id: "perfYtd",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "YTD"),name: "Perf % YTD",type: "number,",},{ id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },{id: "volatility1m",cellRenderer: PercentCell,headerRenderer: makePerfHeaderCell("Volatility", "1m"),name: "Volatility 1M",type: "number,",},];export default function ColumnBase() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,headerGroupHeight: 30,columnBase: { width: 80 },columnGroupExpansions: { "Market Info": false },});const view = grid.view.useValue();return (<div><ColumnPills grid={grid} /><div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group")return (<Grid.HeaderGroupCellcell={c}key={c.idOccurrence}className="text-xs! group flex items-center px-2"><div className="flex-1">{c.id}</div><buttonclassName="text-ln-gray-90 hidden cursor-pointer items-center justify-center text-base group-data-[ln-collapsible=true]:flex"onClick={() => grid.api.columnToggleGroup(c.id)}><ChevronLeftIcon className="hidden group-data-[ln-collapsed=false]:block" /><ChevronRightIcon className="block group-data-[ln-collapsed=false]:hidden" /></button></Grid.HeaderGroupCell>);return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("text-ln-gray-60! dark:text-ln-gray-70! flex h-full w-full items-center text-nowrap px-2 text-xs capitalize",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className="text-xs! flex h-full w-full items-center px-2"/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div></div>);}
import "./component.css";import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";import {exchanges,networks,symbols,} from "@1771technologies/grid-sample-data/dex-pairs-performance";export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function SymbolCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const ticker = row.data.symbolTicker;const symbol = row.data.symbol;const image = symbols[row.data.symbolTicker];return (<div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for symbol ${symbol}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="bg-ln-gray-20 text-ln-gray-100 flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">{ticker}</div><div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div></div>);}export function NetworkCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const name = row.data.network;const image = networks[name];return (<div className="grid grid-cols-[20px_1fr] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for network ${name}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="w-full overflow-hidden text-ellipsis">{name}</div></div>);}export function ExchangeCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const name = row.data.exchange;const image = exchanges[name];return (<div className="grid grid-cols-[20px_1fr] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for exchange ${name}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="w-full overflow-hidden text-ellipsis">{name}</div></div>);}export function PercentCellPositiveNegative({grid: { api },column,row,}: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const field = api.columnField(column, row);if (typeof field !== "number") return "-";const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";return (<divclassName={tw("h-ful flex w-full items-center justify-end tabular-nums",field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",)}>{value}</div>);}export function PercentCell({grid: { api },column,row,}: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const field = api.columnField(column, row);if (typeof field !== "number") return "-";const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;}export const makePerfHeaderCell = (name: string, subname: string) => {return (_: HeaderCellRendererParams<DEXPerformanceData>) => {return (<div className="flex h-full w-full flex-col items-end justify-center"><div>{name}</div><div className="font-mono uppercase">{subname}</div></div>);};};
import { GridBox } from "@1771technologies/lytenyte-pro";import { type JSX, useState, type PropsWithChildren, type ReactNode } from "react";import {CollapseGroupIcon,ColumnsIcon,DragDotsSmallIcon,ExpandGroupIcon,} from "@1771technologies/lytenyte-pro/icons";import type { Grid } from "@1771technologies/lytenyte-pro/types";import { tw } from "./components";export function ColumnPills<T>({ grid }: { grid: Grid<T> }) {const { rootProps, items } = GridBox.useColumnBoxItems({grid,draggable: true,orientation: "horizontal",placeholder: (el) => el.firstElementChild! as HTMLElement,onDrop: (p) => {if (p.src.id === p.target.id) return;grid.api.columnMove({moveColumns: [p.src],moveTarget: p.target,before: p.isBefore,});},});return (<GridBox.Root {...rootProps}><PillManagerRow icon={<ColumnsIcon />} label="Columns">{items.map((c) => {return (<GridBox.Itemkey={c.id}item={c}className={tw("flex h-[52px] items-center", "horizontal-indicators")}onKeyDown={(e) => {if (e.key === " " &&((e.target as HTMLElement).parentElement as HTMLElement) === e.currentTarget) {grid.api.columnUpdate({[c.data.id]: { hide: !c.data.hide },});}}}itemClassName={tw("h-full flex items-center px-[6px] focus:outline-none group text-ln-gray-90 ","opacity-60 hover:opacity-80 transition-opacity",!c.data.hide && "opacity-100 hover:opacity-100",)}><divonClick={(e) => {if (e.currentTarget.contains(e.target as HTMLElement))grid.api.columnUpdate({[c.data.id]: { hide: !c.data.hide },});}}className="bg-ln-pill-column-fill border-ln-pill-column-stroke group-focus-visible:ring-ln-primary-50 flex h-7 cursor-pointer items-center text-nowrap rounded border pl-1 group-focus-visible:ring-1"><DragDotsSmallIcon className="no-drag cursor-grab" /><div className="pl-1 pr-3 text-xs">{c.label}</div></div></GridBox.Item>);})}</PillManagerRow></GridBox.Root>);}export interface PillManagerRowProps {readonly icon: ReactNode;readonly label: string;readonly className?: string;}export function PillManagerRow({icon,label,children,className,}: PropsWithChildren<PillManagerRowProps>) {const [expanded, setExpanded] = useState(false);return (<divclassName={tw("bg-ln-gray-05 border-ln-gray-20 grid grid-cols-[42px_1fr_64px] border-t md:grid-cols-[151px_1fr_40px]",)}><div className="text-ln-gray-80 flex min-h-[52px] items-center justify-center gap-2 text-sm md:justify-start md:pl-[30px] md:pr-3">{icon}<div className="hidden md:block">{label}</div></div><GridBox.PanelclassName={tw("no-scrollbar flex max-h-[200px] w-full items-center overflow-auto focus:outline-none md:max-h-[unset]","focus-visible:outline-ln-primary-50 focus-visible:outline focus-visible:-outline-offset-1",expanded && "flex-wrap",className,)}>{children}</GridBox.Panel><divclassName={tw("border-ln-gray-30 relative flex items-center justify-center gap-1 border-l","before:bg-linear-to-tr before:absolute before:-left-1 before:h-full before:w-1 before:from-transparent before:to-[rgba(0,0,0,0.075)]",)}><GridIconButton onClick={() => setExpanded((prev) => !prev)} className="h-7 min-w-7">{expanded ? (<CollapseGroupIcon width={20} height={20} />) : (<ExpandGroupIcon width={20} height={20} />)}</GridIconButton></div></div>);}export function GridIconButton(props: JSX.IntrinsicElements["button"]) {return (<button{...props}className={tw("hover:bg-ln-gray-20 text-ln-gray-70 focus-visible:outline-ln-primary-50 flex size-7 min-h-6 min-w-6 cursor-pointer items-center justify-center rounded transition-colors focus:outline-none focus-visible:outline-1 focus-visible:outline-offset-[-3px]",props.className,)}></button>);}
Next Steps
- Column Resizing: Change column widths programmatically or through user interaction.
- Column ID & Name: Define user-friendly column names and ensure unique IDs.
- Column Moving: Reorder columns programmatically or through drag-and-drop.
- Column Groups: Create a column header hierarchy and associate columns with column groups.