Shadcn Table
componentdata-grid-row-height-menusource: ok · hit
Data Grid Row Height Menu
A row height menu component for the data grid to adjust row sizes
npx shadcn add "https://tablecn.com/r/data-grid-row-height-menu.json"free
Interaction
dropdown
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
select
Files3 · source cached 10/9/2026, 1:26:34 PM
src/registry/bases/base/components/data-grid/data-grid-row-height-menu.tsx
"use client";
import type * as React from "react";
import { type RowData, Subscribe, type Table } from "@tanstack/react-table";
import type { DataGridFeatures } from "@/lib/data-grid-features";
import type { RowHeightValue } from "@/lib/data-grid-types";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/bases/base/ui/select";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";
const rowHeights = [
{
label: "Short",
value: "short" as const,
icon: (props: React.ComponentProps<"svg">) => (
<IconPlaceholder
lucide="MinusIcon"
tabler="IconMinus"
hugeicons="MinusSignIcon"
phosphor="MinusIcon"
remixicon="RiSubtractLine"
{...props}
/>
),
},
{
label: "Medium",
value: "medium" as const,
icon: (props: React.ComponentProps<"svg">) => (
<IconPlaceholder
lucide="EqualIcon"
tabler="IconMinus"
hugeicons="MinusSignIcon"
phosphor="MinusIcon"
remixicon="RiSubtractLine"
{...props}
/>
),
},
{
label: "Tall",
value: "tall" as const,
icon: (props: React.ComponentProps<"svg">) => (
<IconPlaceholder
lucide="AlignVerticalSpaceAroundIcon"
tabler="IconLayoutRows"
hugeicons="LayoutIcon"
phosphor="RowsIcon"
remixicon="RiLayoutLine"
{...props}
/>
),
},
{
label: "Extra Tall",
value: "extra-tall" as const,
icon: (props: React.ComponentProps<"svg">) => (
<IconPlaceholder
lucide="ChevronsDownUpIcon"
tabler="IconSelector"
hugeicons="UnfoldMoreIcon"
phosphor="ArrowsVerticalIcon"
remixicon="RiArrowUpDownLine"
{...props}
/>
),
},
] as const;
interface DataGridRowHeightMenuProps<
TData extends RowData,
> extends React.ComponentProps<typeof SelectContent> {
table: Table<DataGridFeatures, TData>;
disabled?: boolean;
}
export function DataGridRowHeightMenu<TData extends RowData>({
table,
disabled,
...props
}: DataGridRowHeightMenuProps<TData>) {
return (
<Subscribe source={table.atoms.rowHeight}>
{(rowHeight) => {
const selectedRowHeight =
rowHeights.find((option) => option.value === rowHeight) ??
rowHeights[0];
return (
<Select
value={rowHeight}
onValueChange={(value: RowHeightValue | null) => {
if (value == null) return;
table.setRowHeight(value);
}}
disabled={disabled}
>
<SelectTrigger className="[&_svg:nth-child(2)]:hidden">
<SelectValue placeholder="Row height">
<selectedRowHeight.icon />
{selectedRowHeight.label}
</SelectValue>
</SelectTrigger>
<SelectContent {...props}>
<SelectGroup>
{rowHeights.map((option) => (
<SelectItem key={option.value} value={option.value}>
<option.icon />
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
);
}}
</Subscribe>
);
}
src/lib/data-grid-features.ts
import {
assignPrototypeAPIs,
assignTableAPIs,
type CellData,
columnFilteringFeature,
columnOrderingFeature,
columnPinningFeature,
columnResizingFeature,
columnSizingFeature,
columnVisibilityFeature,
createFilteredRowModel,
createSortedRowModel,
functionalUpdate,
makeStateUpdater,
metaHelper,
type OnChangeFn,
type RowData,
rowSelectionFeature,
rowSortingFeature,
type Table,
type TableFeature,
type TableFeatures,
tableFeatures,
type Updater,
} from "@tanstack/react-table";
import type {
CellPosition,
DataGridColumnMeta,
DataGridTableMeta,
RowHeightValue,
} from "@/lib/data-grid-types";
import { getLineCount, getRowHeightValue } from "@/lib/data-grid-utils";
const DEFAULT_ROW_HEIGHT: RowHeightValue = "short";
interface TableState_DataGridRowHeight {
rowHeight: RowHeightValue;
}
interface TableOptions_DataGridRowHeight {
onRowHeightChange?: OnChangeFn<RowHeightValue>;
}
interface Table_DataGridRowHeight {
getRowHeight: () => RowHeightValue;
getRowSize: () => number;
getRowLineCount: () => number;
setRowHeight: (updater: Updater<RowHeightValue>) => void;
resetRowHeight: (defaultState?: boolean) => void;
}
interface TableState_DataGridCellEditing {
editingCell: CellPosition | null;
}
interface TableOptions_DataGridCellEditing {
enableCellEditing?: boolean;
onEditingCellChange?: OnChangeFn<CellPosition | null>;
}
interface ColumnDef_DataGridCellEditing {
enableCellEditing?: boolean;
}
interface Table_DataGridCellEditing {
getEditingCell: () => CellPosition | null;
setEditingCell: (updater: Updater<CellPosition | null>) => void;
resetEditingCell: (defaultState?: boolean) => void;
}
interface Column_DataGridCellEditing {
getCanEdit: () => boolean;
}
interface Cell_DataGridCellEditing {
getCanEdit: () => boolean;
getIsEditing: () => boolean;
}
declare module "@tanstack/react-table" {
interface Plugins {
dataGridRowHeightFeature: TableFeature;
dataGridCellEditingFeature: TableFeature;
}
interface TableState_FeatureMap {
dataGridRowHeightFeature: TableState_DataGridRowHeight;
dataGridCellEditingFeature: TableState_DataGridCellEditing;
}
interface TableOptions_FeatureMap<
in out TFeatures extends TableFeatures,
in out TData extends RowData,
> {
dataGridRowHeightFeature: TableOptions_DataGridRowHeight;
dataGridCellEditingFeature: TableOptions_DataGridCellEditing;
}
interface ColumnDef_FeatureMap<
in out TFeatures extends TableFeatures,
in out TData extends RowData,
TValue extends CellData,
> {
dataGridCellEditingFeature: ColumnDef_DataGridCellEditing;
}
interface Table_FeatureMap<
in out TFeatures extends TableFeatures,
in out TData extends RowData,
> {
dataGridRowHeightFeature: Table_DataGridRowHeight;
dataGridCellEditingFeature: Table_DataGridCellEditing;
}
interface Column_FeatureMap<
in out TFeatures extends TableFeatures,
in out TData extends RowData,
> {
dataGridCellEditingFeature: Column_DataGridCellEditing;
}
interface Cell_FeatureMap {
dataGridCellEditingFeature: Cell_DataGridCellEditing;
}
}
type DataGridInstance = Table<DataGridFeatures, RowData>;
function asDataGrid(table: object) {
return table as DataGridInstance;
}
const dataGridRowHeightFeature: TableFeature = {
getInitialState: (initialState) => ({
rowHeight: DEFAULT_ROW_HEIGHT,
...initialState,
}),
getDefaultTableOptions: (table) => {
const options: TableOptions_DataGridRowHeight = {
onRowHeightChange: makeStateUpdater("rowHeight", table),
};
return options;
},
constructTableAPIs: (table) => {
const instance = asDataGrid(table);
const setRowHeight = (updater: Updater<RowHeightValue>) =>
instance.options.onRowHeightChange?.((old) =>
functionalUpdate(updater, old),
);
assignTableAPIs("dataGridRowHeightFeature", table, {
table_getRowHeight: {
fn: () => instance.atoms.rowHeight.get(),
},
table_getRowSize: {
fn: () => getRowHeightValue(instance.atoms.rowHeight.get()),
},
table_getRowLineCount: {
fn: () => getLineCount(instance.atoms.rowHeight.get()),
},
table_setRowHeight: { fn: setRowHeight },
table_resetRowHeight: {
fn: (defaultState?: boolean) =>
setRowHeight(
defaultState
? DEFAULT_ROW_HEIGHT
: (instance.initialState.rowHeight ?? DEFAULT_ROW_HEIGHT),
),
},
});
},
};
function getCanEditColumn(column: {
table: object;
columnDef: ColumnDef_DataGridCellEditing;
}) {
return (
asDataGrid(column.table).options.enableCellEditing !== false &&
column.columnDef.enableCellEditing !== false
);
}
const dataGridCellEditingFeature: TableFeature = {
getInitialState: (initialState) => ({
editingCell: null,
...initialState,
}),
getDefaultTableOptions: (table) => {
const options: TableOptions_DataGridCellEditing = {
onEditingCellChange: makeStateUpdater("editingCell", table),
};
return options;
},
constructTableAPIs: (table) => {
const instance = asDataGrid(table);
const setEditingCell = (updater: Updater<CellPosition | null>) =>
instance.options.onEditingCellChange?.((old) =>
functionalUpdate(updater, old),
);
assignTableAPIs("dataGridCellEditingFeature", table, {
table_getEditingCell: {
fn: () => instance.atoms.editingCell.get(),
},
table_setEditingCell: { fn: setEditingCell },
table_resetEditingCell: {
fn: (defaultState?: boolean) =>
setEditingCell(
defaultState ? null : (instance.initialState.editingCell ?? null),
),
},
});
},
assignColumnPrototype: (prototype, table) => {
assignPrototypeAPIs("dataGridCellEditingFeature", prototype, table, {
column_getCanEdit: { fn: getCanEditColumn },
});
},
assignCellPrototype: (prototype, table) => {
assignPrototypeAPIs("dataGridCellEditingFeature", prototype, table, {
cell_getCanEdit: {
fn: (cell: { column: Parameters<typeof getCanEditColumn>[0] }) =>
getCanEditColumn(cell.column),
},
cell_getIsEditing: {
fn: (cell: {
table: object;
row: { id: string };
column: { id: string };
}) => {
const editingCell = asDataGrid(cell.table).atoms.editingCell.get();
return (
editingCell?.rowId === cell.row.id &&
editingCell.columnId === cell.column.id
);
},
},
});
},
};
export const dataGridFeatures = tableFeatures({
columnFilteringFeature,
columnOrderingFeature,
columnPinningFeature,
columnResizingFeature,
columnSizingFeature,
columnVisibilityFeature,
rowSelectionFeature,
rowSortingFeature,
dataGridRowHeightFeature,
dataGridCellEditingFeature,
filteredRowModel: createFilteredRowModel(),
sortedRowModel: createSortedRowModel(),
tableMeta: metaHelper<DataGridTableMeta>(),
columnMeta: metaHelper<DataGridColumnMeta>(),
});
export type DataGridFeatures = typeof dataGridFeatures;
src/lib/data-grid-types.ts
import type { Cell, RowData } from "@tanstack/react-table";
import type { DataGridFeatures } from "@/lib/data-grid-features";
export type Direction = "ltr" | "rtl";
export type RowHeightValue = "short" | "medium" | "tall" | "extra-tall";
export interface CellSelectOption {
label: string;
value: string;
icon?: React.ComponentType<React.ComponentProps<"svg">>;
count?: number;
}
export type CellOpts =
| {
variant: "short-text";
}
| {
variant: "long-text";
}
| {
variant: "number";
min?: number;
max?: number;
step?: number;
}
| {
variant: "select";
options: CellSelectOption[];
}
| {
variant: "multi-select";
options: CellSelectOption[];
}
| {
variant: "checkbox";
}
| {
variant: "date";
}
| {
variant: "url";
}
| {
variant: "file";
maxFileSize?: number;
maxFiles?: number;
accept?: string;
multiple?: boolean;
};
export interface CellUpdate {
rowId: string;
columnId: string;
value: unknown;
}
export interface DataGridColumnMeta {
label?: string;
cell?: CellOpts;
}
export interface DataGridTableMeta {
dataGridRef?: React.RefObject<HTMLElement | null>;
cellMapRef?: React.RefObject<Map<string, HTMLDivElement>>;
focusedCell?: CellPosition | null;
editingCell?: CellPosition | null;
selectionState?: SelectionState;
searchOpen?: boolean;
getIsCellSelected?: (rowId: string, columnId: string) => boolean;
getIsSearchMatch?: (rowId: string, columnId: string) => boolean;
getIsActiveSearchMatch?: (rowId: string, columnId: string) => boolean;
scrollToCell?: (
rowId: string,
columnId: string,
align?: "auto" | "start" | "center" | "end",
) => void;
onRowSelect?: (rowId: string, checked: boolean, shiftKey: boolean) => void;
onDataUpdate?: (params: CellUpdate | Array<CellUpdate>) => void;
onRowsDelete?: (rowIds: string[]) => void | Promise<void>;
onColumnClick?: (columnId: string) => void;
onCellClick?: (
rowId: string,
columnId: string,
event?: React.MouseEvent,
) => void;
onCellDoubleClick?: (rowId: string, columnId: string) => void;
onCellMouseDown?: (
rowId: string,
columnId: string,
event: React.MouseEvent,
) => void;
onCellMouseEnter?: (rowId: string, columnId: string) => void;
onCellMouseUp?: () => void;
onCellContextMenu?: (
rowId: string,
columnId: string,
event: React.MouseEvent,
) => void;
onCellEditingStart?: (rowId: string, columnId: string) => void;
onCellEditingStop?: (opts?: {
direction?: NavigationDirection;
moveToNextRow?: boolean;
}) => void;
onCellsCopy?: () => void;
onCellsCut?: () => void;
onCellsPaste?: (expand?: boolean) => void;
onSelectionClear?: () => void;
onFilesUpload?: (params: {
files: File[];
rowId: string;
columnId: string;
}) => Promise<FileCellData[]>;
onFilesDelete?: (params: {
fileIds: string[];
rowId: string;
columnId: string;
}) => void | Promise<void>;
contextMenu?: ContextMenuState;
onContextMenuOpenChange?: (open: boolean) => void;
pasteDialog?: PasteDialogState;
onPasteDialogOpenChange?: (open: boolean) => void;
readOnly?: boolean;
}
export interface CellPosition {
rowId: string;
columnId: string;
}
export interface CellRange {
start: CellPosition;
end: CellPosition;
}
export interface SelectionState {
selectedCells: Set<string>;
selectionRange: CellRange | null;
isSelecting: boolean;
}
export interface ContextMenuState {
open: boolean;
x: number;
y: number;
}
export interface PasteDialogState {
open: boolean;
rowsNeeded: number;
clipboardText: string;
}
export type NavigationDirection =
| "up"
| "down"
| "left"
| "right"
| "home"
| "end"
| "ctrl+up"
| "ctrl+down"
| "ctrl+home"
| "ctrl+end"
| "pageup"
| "pagedown"
| "pageleft"
| "pageright"
| "tab"
| "shift+tab";
export interface SearchState {
searchMatches: CellPosition[];
matchIndex: number;
searchOpen: boolean;
onSearchOpenChange: (open: boolean) => void;
searchQuery: string;
onSearchQueryChange: (query: string) => void;
onSearch: (query: string) => void;
onNavigateToNextMatch: () => void;
onNavigateToPrevMatch: () => void;
}
export interface DataGridCellProps<TData extends RowData> {
cell: Cell<DataGridFeatures, TData>;
tableMeta: DataGridTableMeta;
rowId: string;
columnId: string;
rowHeight: RowHeightValue;
isEditing: boolean;
isFocused: boolean;
isSelected: boolean;
isSearchMatch: boolean;
isActiveSearchMatch: boolean;
readOnly: boolean;
}
export interface FileCellData {
id: string;
name: string;
size: number;
type: string;
url?: string;
}
export type TextFilterOperator =
| "contains"
| "notContains"
| "equals"
| "notEquals"
| "startsWith"
| "endsWith"
| "isEmpty"
| "isNotEmpty";
export type NumberFilterOperator =
| "equals"
| "notEquals"
| "lessThan"
| "lessThanOrEqual"
| "greaterThan"
| "greaterThanOrEqual"
| "isBetween"
| "isEmpty"
| "isNotEmpty";
export type DateFilterOperator =
| "equals"
| "notEquals"
| "before"
| "after"
| "onOrBefore"
| "onOrAfter"
| "isBetween"
| "isEmpty"
| "isNotEmpty";
export type SelectFilterOperator =
| "is"
| "isNot"
| "isAnyOf"
| "isNoneOf"
| "isEmpty"
| "isNotEmpty";
export type BooleanFilterOperator = "isTrue" | "isFalse";
export type FilterOperator =
| TextFilterOperator
| NumberFilterOperator
| DateFilterOperator
| SelectFilterOperator
| BooleanFilterOperator;
export interface FilterValue {
operator: FilterOperator;
value?: string | number | string[];
endValue?: string | number;
}