# DataGrid

Import: `import { DataGrid } from '@neo4j-ndl/react'`

## Props

### DataGrid

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `components` | `DataGridComponentsInterface<T>` |  |  | Table sub-component overrides |
| `isAutoResizingColumns` | `boolean` |  | `true` | Enables automatic resizing of columns. If `isResizable` is `true`, the columns will only be automatically resized on initial render of the table. If `isResizable` is `false`, the columns will be resized automatically to always fit the table width (assuming that column maxWidths allow for it). Note: Action columns are a fixed size of 40px, and Pinned columns are not resized automatically. @defaultValue true |
| `isKeyboardNavigable` | `boolean` |  | `true` | Enables keyboard navigation. To control keyboard navigation programmatically for edge cases, pass a `ref` to `tableNavRef` |
| `isLoading` | `boolean` |  | `false` | Enables table's loading state |
| `isResizable` | `boolean` |  | `true` | Resizing the width of columns |
| `isSkeletonLoading` | `boolean` |  | `false` |  |
| `portalTarget` | `HTMLElement \| null` |  | `document.body` | The target element for portaled elements to be rendered in, e.g. the modal root if the data grid is inside a modal. This is used in for example the DropdownCell, RowsPerPage, and ColumnControls. |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |
| `rootProps` | `HtmlAttributes<"div">` |  |  | @deprecated Use htmlAttributes, className and style on the root element instead |
| `skeletonProps` | `(SkeletonProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>)` |  |  |  |
| `styleRules` | `{ apply: { color?: { value: string; isDisabled?: boolean; }; colorRange?: { maxColor: string; maxValue: number; minColor: string; minValue: number; onColumn: string; isDisabled?: boolean \| undefined; midColor?: string \| undefined; midValue?: number \| undefined; } \| undefined; textColor?: { .....` |  |  | Rule-based conditional styling for cells and rows. |
| `styleRuleValueConverters` | `DataGridStyleRuleValueConverters` |  |  | Per-column converters that teach the style-rule engine how to interpret complex cell values. |
| `styling` | `Partial<{ hasZebraStriping: boolean; borderStyle: "all-sides" \| "horizontal" \| "vertical" \| "none"; headerStyle: "filled" \| "clean"; hasHoverEffects: boolean; isCompact: boolean; }>` |  | `{}` |  |
| `tableInstance` | `Table<T>` | ✅ |  | Table instance created from `useTable` |
| `tableNavRef` | `RefObject<any>` |  |  | Reference to the tableNav instance for programmatic control |

### DataGrid.ActionButton

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `action` | `MenuItemProps` | ✅ |  |  |
| `as` | `ElementType<any, keyof IntrinsicElements>` |  |  | An override of the default HTML tag of the root of the component. Can also be another React component. |
| `onClose` | `() => void` | ✅ |  |  |
| `ref` | `any` |  |  | A ref to apply to the root element. |

### DataGrid.ActionCell

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `cell` | `CellContext<T, unknown>` |  |  |  |
| `innerCleanIconButtonProps` | `Partial<Omit<CleanIconButtonButtonProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>, "ref">>` |  |  |  |
| `innerMenuProps` | `Partial<MenuProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>>` |  |  |  |

### DataGrid.Body

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.BodyCell

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `cell` | `Cell<T, unknown>` | ✅ |  |  |
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.BodyRow

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |
| `row` | `Row<T>` | ✅ |  |  |

### DataGrid.ColumnControls

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `cell` | `Header<T, unknown>` | ✅ |  |  |
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.DropDownCell

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `ariaLabel` | `string` | ✅ |  | Aria label of the select component. Required for accessibility. |
| `cell` | `CellContext<T, unknown>` | ✅ |  |  |
| `components` | `Partial<SelectComponents<DropDownCellOption, boolean, GroupBase<DropDownCellOption>>>` |  |  | Override the underlying react-select components (e.g. `Option`, `SingleValue`, `MultiValue`). Merged over the Needle defaults, so only the components you pass are replaced. |
| `errorText` | `ReactNode` |  |  | Error message. If it exists the select will be in error state. |
| `helpText` | `ReactNode` |  |  | Help text of the select component. |
| `isDisabled` | `boolean` |  |  | Whether the select component is disabled |
| `options` | `{ value: string; label: string; }[]` | ✅ |  |  |
| `portalTarget` | `HTMLElement` |  | `document.body` | The target element to portal the menu of the select component to, e.g. the modal root if the data grid is inside a modal. Defaults to the portalTarget prop of the DataGrid root component. |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.Header

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.HeaderCell

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `cell` | `Header<T, unknown>` | ✅ |  |  |
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.HeaderTitle

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `cell` | `Header<T, unknown>` | ✅ |  |  |
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.InlineEditCell

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `ariaLabel` | `string` | ✅ |  |  |
| `cell` | `CellContext<T, unknown>` | ✅ |  |  |
| `ref` | `Ref<HTMLInputElement>` |  |  | A ref to apply to the root element. |
| `value` | `string` | ✅ |  |  |

### DataGrid.LoadingPlaceholder

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.Navigation

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.NoDataIcon

_No props documented._
### DataGrid.NoDataPlaceholder

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.NoDataPlaceholderContentWrapper

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.Pagination

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLElement>` |  |  | A ref to apply to the root element. |

### DataGrid.PaginationArrowButton

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `action` | `'next' \| 'previous'` | ✅ |  |  |
| `children` | `ReactNode` |  |  |  |
| `onClick` | `(e: MouseEvent<HTMLButtonElement, MouseEvent>) => void` |  |  |  |
| `ref` | `Ref<HTMLButtonElement>` |  |  | A ref to apply to the root element. |

### DataGrid.PaginationNumericButton

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` |  |  |  |
| `currentIndex` | `string \| number` | ✅ |  |  |
| `isSelected` | `boolean` | ✅ |  |  |
| `onClick` | `(e: MouseEvent<HTMLButtonElement, MouseEvent>) => void` |  |  |  |
| `ref` | `Ref<HTMLButtonElement>` |  |  | A ref to apply to the root element. |

### DataGrid.ResizingBar

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `header` | `Header<T, unknown>` | ✅ |  |  |
| `ref` | `Ref<HTMLButtonElement>` |  |  | A ref to apply to the root element. |

### DataGrid.RowsPerPage

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` |  |  |  |
| `portalTarget` | `HTMLElement` |  | `document.body` | The target element to portal the menu of the select component to. Should be document.body normally, or the modal root if the select is inside a modal. Defaults to the portalTarget prop of the DataGrid root component. |
| `ref` | `Ref<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.Scrollable

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` |  |  |  |
| `ref` | `Ref<HTMLDivElement> & RefObject<HTMLDivElement>` |  |  | A ref to apply to the root element. |

### DataGrid.TableResults

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` |  |  |  |
| `manualPagination` | `{ from: number; to: number; totalRows: number; }` |  |  |  |
| `ref` | `Ref<HTMLSpanElement>` |  |  | A ref to apply to the root element. |
| `resultsLabel` | `string` |  |  | The label to use for the results, meaning the word "results" in the sentence "Showing X-Y of Z results". @defaultValue 'results' |

## Examples

### Datagrid Column Filtering

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  useReactTable,
} from '@tanstack/react-table';

import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const Component = () => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const columns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Name',
      maxSize: 300,
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      footer: (info) => info.column.id,
      header: () => 'Age',
      minSize: 80,
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Cypher Command',
      minSize: 400,
      size: 400,
    }),
  ];

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns,
    data: TABLE_DEMO_DATA,
    debugColumns: true,
    debugHeaders: true,
    debugTable: true,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: false,
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    initialState: {
      pagination: {
        pageSize: 5,
      },
    },
  });

  return (
    <div className="n-w-full n-bg-light-neutral-text-weakest">
      <DataGrid
        isResizable={true}
        tableInstance={table}
        isKeyboardNavigable={true}
      />
    </div>
  );
};

export default Component;
```

### Datagrid Compact Mode

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from '@tanstack/react-table';

import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const Component = () => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const columns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Name',
      maxSize: 300,
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      footer: (info) => info.column.id,
      header: () => 'Age',
      minSize: 80,
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Cypher Command',
      minSize: 400,
      size: 400,
    }),
  ];

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns,
    data: TABLE_DEMO_DATA,
    debugColumns: true,
    debugHeaders: true,
    debugTable: true,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: true,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    initialState: {
      pagination: {
        pageSize: 5,
      },
    },
  });

  return (
    <div className="n-w-full n-bg-light-neutral-text-weakest">
      <DataGrid
        isResizable={false}
        tableInstance={table}
        isKeyboardNavigable={false}
        styling={{
          borderStyle: 'all-sides',
          hasHoverEffects: true,
          hasZebraStriping: false,
          isCompact: true,
        }}
      />
    </div>
  );
};

export default Component;
```

### Datagrid Component Overriding

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid, Tooltip, Typography } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from '@tanstack/react-table';

import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const Component = () => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const columns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Name',
      maxSize: 300,
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      footer: (info) => info.column.id,
      header: () => 'Age',
      minSize: 80,
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Cypher Command',
      minSize: 400,
      size: 400,
    }),
  ];

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns,
    data: TABLE_DEMO_DATA,
    debugColumns: true,
    debugHeaders: true,
    debugTable: true,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: true,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    initialState: {
      pagination: {
        pageSize: 5,
      },
    },
  });

  return (
    <div className="n-flex n-flex-col n-gap-token-8">
      <Typography
        variant="body-medium"
        as="div"
        className="n-flex n-flex-col n-gap-token-8 n-p-token-24 n-pb-token-16"
      >
        <div>Current container height: 400px</div>
        <div>Try updating height with Storybook&apos; s story controls👇</div>
      </Typography>

      <div
        style={{
          height: '400px',
        }}
      >
        <DataGrid
          className="n-h-full"
          isResizable={true}
          tableInstance={table}
          components={{
            Body: (props) => <DataGrid.Body {...props} />,
            HeaderTitle: ({ cell, children }) => {
              const { column, id } = cell;
              const title =
                typeof column.columnDef.header === 'string'
                  ? column.columnDef.header
                  : id;
              return (
                <Tooltip type="simple">
                  <Tooltip.Trigger hasButtonWrapper>
                    <span>{children}</span>
                  </Tooltip.Trigger>
                  <Tooltip.Content>{title}</Tooltip.Content>
                </Tooltip>
              );
            },
            PaginationNumericButton: ({ isSelected, style, ...restProps }) => {
              return (
                <DataGrid.PaginationNumericButton
                  {...restProps}
                  isSelected={isSelected}
                  style={{
                    ...style,
                    ...(isSelected && {
                      backgroundImage:
                        'linear-gradient(90deg, #00C0FF 0%, #FFCF00 20%, #FC4F4F 40%, #00C0FF 100%)',
                      backgroundSize: '200% auto',
                      borderRadius: '10px',
                      boxShadow: '0 0 20px #eee',
                    }),
                  }}
                />
              );
            },
            TableResults: () => (
              <DataGrid.TableResults>
                A table with a fixed height of 100px, custom result section and
                custom pagination button
              </DataGrid.TableResults>
            ),
          }}
        />
      </div>
    </div>
  );
};

export default Component;
```

### Datagrid Custom Cell With Style Rules

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import {
  DataGrid,
  type DataGridStyleRule,
  useDataGridCellStyle,
} from '@neo4j-ndl/react';
import {
  type Cell,
  createColumnHelper,
  flexRender,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  type RowData,
  useReactTable,
} from '@tanstack/react-table';

import { TABLE_DEMO_DATA, type TestDataFormat } from './staticData';

const styleRules: DataGridStyleRule[] = [
  // Row coloring: all cells styled when name contains "Alicia"
  {
    apply: {
      color: { value: '#000000' },
      textColor: { value: '#ffffff' },
    },
    column: null,
    priority: 3,
    where: { contains: [{ column: 'name' }, 'Alicia'] },
  },
  // Cell coloring: cells in the "age" column with value 51 get green
  {
    apply: { color: { value: '#00bb00' } },
    column: 'age',
    priority: 3,
    where: { equal: [{ column: 'age' }, 51] },
  },
  // Dual color ranges on the "age" column: independent gradients for background
  // (driven by age, full 0-100 range) and text color (driven by age too, but
  // with a narrower 20-80 band so the text contrast is calibrated separately).
  {
    apply: {
      colorRange: {
        maxColor: '#f00000',
        maxValue: 100,
        midColor: '#a00000',
        midValue: 50,
        minColor: '#800000',
        minValue: 0,
        onColumn: 'age',
      },
      textColorRange: {
        maxColor: '#aaaaaa',
        maxValue: 80,
        minColor: '#ffffff',
        minValue: 20,
        onColumn: 'age',
      },
    },
    column: 'age',
    priority: 2, // Lower priority so explicit green rule overrides
  },
  // Multi-column rule: all cells styled when age > 50 AND name contains "o"
  {
    apply: {
      color: { value: '#fef3c7' },
      textColor: { value: '#000000' },
    },
    column: null,
    priority: 0, // Lower priority so other rules take precedence
    where: {
      and: [
        { greaterThan: [{ column: 'age' }, 50] },
        { contains: [{ column: 'name' }, 'o'] },
      ],
    },
  },
];

/**
 * A custom BodyCell that uses the `useDataGridCellStyle` hook to manually apply style rules to the cell.
 */
const CustomBodyCell = <T extends RowData>({
  cell,
}: {
  cell: Cell<T, unknown>;
}) => {
  const ruleStyle = useDataGridCellStyle(cell);

  return (
    <div
      className="ndl-data-grid-td"
      role="cell"
      style={{
        ...ruleStyle,
        width: cell.column.getSize(),
      }}
    >
      {flexRender(cell.column.columnDef.cell, cell.getContext())}
    </div>
  );
};

const Component = () => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const columns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      header: () => 'Name',
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      header: () => 'Age',
      minSize: 80,
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      header: () => 'Cypher Command',
      minSize: 400,
      size: 400,
    }),
  ];

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns,
    data: TABLE_DEMO_DATA,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: true,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    initialState: {
      pagination: {
        pageSize: 10,
      },
    },
  });

  return (
    <div className="n-w-full n-bg-light-neutral-text-weakest">
      <DataGrid
        isResizable={false}
        tableInstance={table}
        isKeyboardNavigable={false}
        styleRules={styleRules}
        components={{
          BodyCell: CustomBodyCell,
        }}
        styling={{
          borderStyle: 'all-sides',
          hasHoverEffects: true,
          hasZebraStriping: false,
        }}
      />
    </div>
  );
};

export default Component;
```

### Datagrid In Dialog

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import {
  DataGrid,
  type DataGridNav,
  Dialog,
  FilledButton,
  Select,
  useDataGridContext,
} from '@neo4j-ndl/react';
import {
  type ColumnDef,
  type ColumnMeta,
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from '@tanstack/react-table';
import { useRef, useState } from 'react';

import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const columnHelper = createColumnHelper<TestDataFormat>();

const actionsColumn: (tableNav: DataGridNav) => ColumnDef<TestDataFormat> = (
  tableNav,
) => ({
  cell: ({ row }) => {
    const options = ['Edit', 'Delete', 'View'];
    const dropdownOptions = options.map((o) => ({ label: o, value: o }));
    // eslint-disable-next-line react-hooks/rules-of-hooks
    const [selected, setSelected] = useState(dropdownOptions[0]);
    // eslint-disable-next-line react-hooks/rules-of-hooks
    const { portalTarget } = useDataGridContext();

    return (
      <div
        data-testid={`ndl-actions-cell-row-${row.index}`}
        className="n-flex n-flex-row n-gap-token-8"
      >
        <Select
          ariaLabel={`actions-select-row-${row.index}`}
          type="select"
          selectProps={{
            menuPortalTarget: portalTarget,
            menuPosition: 'fixed',
            onChange: (e) => {
              if (!e) {
                return;
              }
              setSelected(e);
            },
            onKeyDown: (e) => {
              // Stop propagation of arrow down to the table, to avoid moving to the next item in the cell
              if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
                e.stopPropagation();
              }
            },
            onMenuClose: () => {
              console.info(
                'Start listening to keyboard events for table navigation',
              );
              tableNav.enable();
            },
            onMenuOpen: () => {
              console.info(
                'Stop listening to keyboard events for table navigation',
              );
              tableNav.disable();
            },
            options: dropdownOptions,
            value: selected,
          }}
        />
      </div>
    );
  },
  header: 'Custom Select',
  id: 'custom-select',
  minSize: 150,
});

const Component = () => {
  const [data, setData] = useState(TABLE_DEMO_DATA);
  const [isAgeFilterApplied, setIsAgeFilterApplied] = useState(false);

  const ColumnActions: ColumnMeta<TestDataFormat, unknown> = {
    columnActions: {
      actions: [
        {
          onClick: () => {
            if (isAgeFilterApplied) {
              setData(TABLE_DEMO_DATA);
              setIsAgeFilterApplied(false);
              return;
            } else {
              const filteredData = data.filter((item) => item.age >= 18);
              setData(filteredData);
              setIsAgeFilterApplied(true);
            }
          },
          title: 'Age >= 18',
        },
      ],
    },
  };

  const regularColumns = [
    columnHelper.accessor('name', {
      cell: (cell) => (
        <DataGrid.InlineEditCell
          cell={cell}
          value={cell.getValue()}
          ariaLabel="edit name"
        />
      ),
      maxSize: 300,
      meta: {
        isInlineEditCell: {
          onEditChange: (newValue, cell) => {
            setData((oldData) => {
              return oldData.map((row) => {
                if (row.name === cell.row.original.name) {
                  return {
                    ...row,
                    name: newValue,
                  };
                }
                return row;
              });
            });
          },
        },
      },
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => {
        return (
          <DataGrid.DropDownCell
            cell={info}
            options={[
              {
                label: '12',
                value: '12',
              },
              {
                label: '32',
                value: '32',
              },
              {
                label: '37',
                value: '37',
              },
              {
                label: '39',
                value: '39',
              },
              {
                label: '51',
                value: '51',
              },
            ]}
            ariaLabel="select age"
            portalTarget={document.getElementById('default-menu')}
          />
        );
      },
      meta: {
        isDropDownCell: {
          onChange: (newValue, cell) => {
            if (newValue === undefined) {
              // No change
              return;
            }

            const parsedValue = parseInt(newValue, 10);

            if (isNaN(parsedValue)) {
              return;
            }

            setData((oldData) => {
              return oldData.map((row) => {
                if (row.name === cell.row.original.name) {
                  return {
                    ...row,
                    age: parsedValue,
                  };
                }
                return row;
              });
            });
          },
        },
        ...ColumnActions,
      },
      minSize: 120,
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      minSize: 400,
      size: 400,
    }),
  ];

  const allColumns = (tableNav: DataGridNav) => [
    ...regularColumns,
    actionsColumn(tableNav),
  ];

  const tableNavRef = useRef<DataGridNav>(null!);
  const table = useReactTable({
    autoResetPageIndex: false,
    columnResizeMode: 'onChange',
    columns: allColumns(tableNavRef.current),
    data: data,
    debugColumns: true,
    debugHeaders: true,
    debugTable: true,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: true,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    initialState: {
      pagination: {
        pageSize: 5,
      },
    },
  });

  // Modal code
  const [isOpen, setIsOpen] = useState<boolean>(false);
  const handleClick = () => setIsOpen((prev) => !prev);
  const handleClose = () => setIsOpen(false);

  // DataGrid code
  return (
    <div className="n-w-full">
      <div className="n-flex n-justify-center">
        <FilledButton onClick={handleClick}>Open Dialog</FilledButton>
      </div>
      <Dialog
        className="n-p-token-8 n-bg-neutral-10 n-max-w-max n-rounded-lg"
        htmlAttributes={{
          id: 'default-menu',
        }}
        onClose={handleClose}
        isOpen={isOpen}
        hasDisabledCloseButton={true}
      >
        <Dialog.Content>
          <DataGrid
            portalTarget={document.getElementById('default-menu')}
            isResizable={true}
            tableInstance={table}
            tableNavRef={tableNavRef}
            isKeyboardNavigable={true}
            styling={{
              borderStyle: 'horizontal',
              hasZebraStriping: false,
            }}
          />
        </Dialog.Content>
      </Dialog>
    </div>
  );
};

export default Component;
```

### Datagrid Keyboard Navigation

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import {
  Checkbox,
  DataGrid,
  type DataGridNav,
  IconButton,
  Select,
  useDataGridContext,
} from '@neo4j-ndl/react';
import { MegaphoneIconOutline } from '@neo4j-ndl/react/icons';
import {
  type ColumnDef,
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  useReactTable,
} from '@tanstack/react-table';
import { useRef, useState } from 'react';

import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const selectableColumn: ColumnDef<TestDataFormat> = {
  cell: ({ row }) => (
    <Checkbox
      ariaLabel={`row-${row.index}`}
      isChecked={row.getIsSelected() || false}
      onChange={row.getToggleSelectedHandler()}
    />
  ),
  header: ({ table }) => (
    <Checkbox
      ariaLabel="Select all rows"
      isChecked={table.getIsAllRowsSelected() || false}
      onChange={table.getToggleAllRowsSelectedHandler()}
      isIndeterminate={table.getIsSomeRowsSelected()}
    />
  ),
  id: 'select',
  maxSize: 80,
  size: 80,
};

const actionsColumn: (tableNav: DataGridNav) => ColumnDef<TestDataFormat> = (
  tableNav,
) => ({
  cell: ({ row }) => {
    const options = ['Edit', 'Delete', 'View'];
    const dropdownOptions = options.map((o) => ({ label: o, value: o }));
    // eslint-disable-next-line react-hooks/rules-of-hooks
    const [selected, setSelected] = useState(dropdownOptions[0]);
    // eslint-disable-next-line react-hooks/rules-of-hooks
    const { portalTarget } = useDataGridContext();

    return (
      <div
        data-testid={`ndl-actions-cell-row-${row.index}`}
        className="n-flex n-flex-row n-gap-token-8"
      >
        <IconButton
          description="Megaphone"
          onClick={() => {
            alert("Let's get loud 📣");
          }}
          htmlAttributes={{
            type: 'button',
          }}
        >
          <MegaphoneIconOutline />
        </IconButton>
        <Select
          ariaLabel={`actions-select-row-${row.index}`}
          type="select"
          selectProps={{
            menuPortalTarget: portalTarget,
            onChange: (e) => {
              if (!e) {
                return;
              }
              setSelected(e);
            },
            onKeyDown: (e) => {
              // Stop propagation of arrow down to the table, to avoid moving to the next item in the cell
              if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
                e.stopPropagation();
              }
            },
            onMenuClose: () => {
              console.info(
                'Start listening to keyboard events for table navigation',
              );
              tableNav.enable();
            },
            onMenuOpen: () => {
              console.info(
                'Stop listening to keyboard events for table navigation',
              );
              tableNav.disable();
            },
            options: dropdownOptions,
            value: selected,
          }}
        />
      </div>
    );
  },
  header: 'actions',
  id: 'actions',
});

const allColumns = (tableNav: DataGridNav) => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const commonColumns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Name',
      maxSize: 300,
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      footer: (info) => info.column.id,
      header: () => 'Age',
      minSize: 80,
    }),
  ];

  return [selectableColumn, ...commonColumns, actionsColumn(tableNav)];
};

const Component = () => {
  const [rowSelection, setRowSelection] = useState({});
  const ref = useRef<HTMLDivElement>(null);
  const tableNavRef = useRef<DataGridNav>(null!);

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns: allColumns(tableNavRef.current),
    data: TABLE_DEMO_DATA,
    enableSorting: false,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    initialState: {
      pagination: {
        pageSize: 5,
      },
    },
    onRowSelectionChange: setRowSelection,
    state: {
      rowSelection,
    },
  });

  return (
    <>
      <DataGrid
        isResizable={false}
        isKeyboardNavigable={true}
        styling={{
          borderStyle: 'all-sides',
        }}
        tableNavRef={tableNavRef}
        tableInstance={table}
        ref={ref}
        portalTarget={document.body}
      />
    </>
  );
};

export default Component;
```

### Datagrid Kitchen Sink

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid, Menu } from '@neo4j-ndl/react';
import { ChevronDownIconSolid } from '@neo4j-ndl/react/icons';
import {
  type ColumnDef,
  type ColumnMeta,
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  type TableOptions,
  useReactTable,
} from '@tanstack/react-table';
import { Leva, useControls } from 'leva';

import { type BorderStyle, type HeaderStyle } from '../DataGrid';
import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const columnHelper = createColumnHelper<TestDataFormat>();

const ColumnActions: ColumnMeta<TestDataFormat, unknown> = {
  columnActions: {
    actions: [
      {
        onClick: () => {
          alert('Action triggered');
        },
        title: 'Example Column Action',
      },
    ],
  },
};

type ColumnGeneratorOptions = {
  shouldIncludeRowActionColumn: boolean;
};
const COMMON_COLUMNS = (options: ColumnGeneratorOptions) => {
  // oxlint-disable-next-line @typescript-eslint/no-explicit-any
  const columns: ColumnDef<TestDataFormat, any>[] = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      maxSize: 300,
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.getValue(),
      header: () => <div>Age</div>,
      minSize: 80,
      ...(options.shouldIncludeRowActionColumn
        ? {
            meta: {
              ...ColumnActions,
            },
          }
        : {}),
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      minSize: 400,
      size: 400,
    }),
  ];

  if (options.shouldIncludeRowActionColumn) {
    columns.push(
      columnHelper.display({
        cell: (cell) => {
          const isSpecialRow =
            cell.row.original.name === TABLE_DEMO_DATA[3].name || undefined;
          return (
            <DataGrid.ActionCell
              cell={cell}
              innerCleanIconButtonProps={{
                children: isSpecialRow && <ChevronDownIconSolid />,
                description: `Actions for ${cell.row.original.name}`,
              }}
              innerMenuProps={
                isSpecialRow && {
                  children: <Menu.Item title="Custom icon and menu props" />,
                }
              }
            />
          );
        },
        id: 'actions',
        meta: {
          isActionCell: {
            actions: [
              {
                onClick: (_event, cell) => {
                  alert(
                    `Row Action triggered for row with index: ${
                      cell.row.index
                    }`,
                  );
                },
                title: 'Example Row Action',
              },
            ],
            onOpenChange: (open) => console.info(`onOpenChange: ${open}`),
          },
        },
      }),
    );
  }

  return columns;
};

const COMMON_TABLE_PROPS = {
  columnResizeMode: 'onChange',
  data: TABLE_DEMO_DATA,
  // https://tanstack.com/table/v8/docs/guide/column-sizing#overview
  defaultColumn: {
    minSize: 80,
  },
  enableSorting: false,
  getCoreRowModel: getCoreRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
  initialState: {
    pagination: {
      pageSize: 5,
    },
  },
  meta: {
    actions: [
      {
        onClick: () => {
          alert('wowowow');
        },
        title: "Let's party",
      },
    ],
  },
} as Omit<TableOptions<TestDataFormat>, 'columns'>;

const Component = () => {
  const {
    keyboard: isKeyboardNavigable,
    zebraStriping: hasZebraStriping,
    borderStyle,
    headerStyle,
    includeRowActionColumn: shouldIncludeRowActionColumn,
  } = useControls({
    borderStyle: {
      options: ['all-sides', 'horizontal', 'vertical', 'none'],
      value: 'all-sides',
    },
    headerStyle: {
      options: ['filled', 'clean'],
      value: 'filled',
    },
    includeRowActionColumn: false,
    keyboard: false,
    zebraStriping: false,
  });

  const table = useReactTable({
    ...COMMON_TABLE_PROPS,
    columns: COMMON_COLUMNS({
      shouldIncludeRowActionColumn,
    }),
    enableSorting: true,
    getSortedRowModel: getSortedRowModel(),
  });

  return (
    <div className="n-h-[1000px] n-w-full n-bg-primary-bg-weak n-p-token-32">
      <Leva isRoot />
      <DataGrid
        isResizable={true}
        tableInstance={table}
        isKeyboardNavigable={isKeyboardNavigable}
        styling={{
          borderStyle: borderStyle as BorderStyle,
          hasZebraStriping,
          headerStyle: headerStyle as HeaderStyle,
        }}
      />
    </div>
  );
};

export default Component;
```

### Datagrid No Pagination Table

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  useReactTable,
} from '@tanstack/react-table';

import { PRUNED_TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const Component = () => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const columns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Name',
      maxSize: 300,
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      footer: (info) => info.column.id,
      header: () => 'Age',
      minSize: 80,
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Cypher Command',
      minSize: 400,
      size: 400,
    }),
  ];

  const table = useReactTable({
    columns,
    data: PRUNED_TABLE_DEMO_DATA,
    enableSorting: false,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <DataGrid
      components={{ Navigation: null }}
      isResizable={false}
      tableInstance={table}
    />
  );
};

export default Component;
```

### Datagrid Percentage Width Columns

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getSortedRowModel,
  useReactTable,
} from '@tanstack/react-table';

import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const Component = () => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const columns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Name',
      size: 200, // 200 / 1000 = 20% of the table width
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      footer: (info) => info.column.id,
      header: () => 'Age',
      minSize: 100, // 100 / 1000 = 10% of the table width
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Cypher Command',
      size: 700, // 700 / 1000 = 70% of the table width
    }),
  ];

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns: columns,
    data: TABLE_DEMO_DATA.slice(0, 5),
    debugColumns: true,
    debugHeaders: true,
    debugTable: true,
    enableSorting: false,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
  });

  // Disable resizing of columns and enable automatic resizing of columns, to allow for percentage width columns
  const isResizable = false;
  const isAutoResizingColumns = true;

  return (
    <div className="n-w-full n-bg-light-neutral-text-weakest">
      <DataGrid
        components={{
          Navigation: null,
        }}
        isAutoResizingColumns={isAutoResizingColumns}
        isKeyboardNavigable={false}
        isResizable={isResizable}
        styling={{
          borderStyle: 'all-sides',
          hasHoverEffects: false,
          hasZebraStriping: false,
        }}
        tableInstance={table}
      />
    </div>
  );
};

export default Component;
```

### Datagrid Placeholder States

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  useReactTable,
} from '@tanstack/react-table';

import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const Component = () => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const columns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Name',
      maxSize: 300,
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      footer: (info) => info.column.id,
      header: () => 'Age',
      minSize: 80,
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Cypher Command',
      minSize: 400,
      size: 400,
    }),
  ];

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns,
    data: TABLE_DEMO_DATA,
    debugColumns: true,
    debugHeaders: true,
    debugTable: true,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: false,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    initialState: {
      pagination: {
        pageSize: 5,
      },
    },
  });

  return (
    <div className="n-w-full n-bg-light-neutral-text-weakest">
      <DataGrid
        isResizable={true}
        tableInstance={table}
        isKeyboardNavigable={true}
      />
    </div>
  );
};

export default Component;
```

### Datagrid Resizable Column

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  useReactTable,
} from '@tanstack/react-table';

import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const Component = ({
  isKeyboardNavigable = true,
  components,
}: {
  isKeyboardNavigable?: boolean;
  components?: React.ComponentProps<typeof DataGrid>['components'];
} = {}) => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const columns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Name',
      maxSize: 300,
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      footer: (info) => info.column.id,
      header: () => 'Age',
      minSize: 80,
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Cypher Command',
      minSize: 400,
      size: 400,
    }),
  ];

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns,
    data: TABLE_DEMO_DATA,
    debugColumns: true,
    debugHeaders: true,
    debugTable: true,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: false,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    initialState: {
      pagination: {
        pageSize: 5,
      },
    },
  });

  return (
    <div className="n-w-full n-bg-light-neutral-text-weakest">
      <DataGrid<TestDataFormat>
        isResizable={true}
        tableInstance={table}
        isKeyboardNavigable={isKeyboardNavigable}
        // @ts-expect-error limitations of storybooks makes it hard to handle generics
        components={components}
      />
    </div>
  );
};

export default Component;
```

### Datagrid Rule Based Styling

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from '@tanstack/react-table';

import { type DataGridStyleRule } from '../style-rules/types';
import { TABLE_DEMO_DATA, type TestDataFormat } from './staticData';

const styleRules: DataGridStyleRule[] = [
  // Row coloring: all cells styled when name contains "Alicia"
  {
    apply: {
      color: { value: '#000000' },
      textColor: { value: '#ffffff' },
    },
    column: null,
    priority: 3,
    where: { contains: [{ column: 'name' }, 'Alicia'] },
  },
  // Cell coloring: cells in the "age" column with value 51 get green
  {
    apply: { color: { value: '#00bb00' } },
    column: 'age',
    priority: 3,
    where: { equal: [{ column: 'age' }, 51] },
  },
  // Dual color ranges on the "age" column: independent gradients for background
  // (driven by age, full 0-100 range) and text color (driven by age too, but
  // with a narrower 20-80 band so the text contrast is calibrated separately).
  {
    apply: {
      colorRange: {
        maxColor: '#f00000',
        maxValue: 100,
        midColor: '#a00000',
        midValue: 50,
        minColor: '#800000',
        minValue: 0,
        onColumn: 'age',
      },
      textColorRange: {
        maxColor: '#aaaaaa',
        maxValue: 80,
        minColor: '#ffffff',
        minValue: 20,
        onColumn: 'age',
      },
    },
    column: 'age',
    priority: 2, // Lower priority so explicit green rule overrides
  },
  // Multi-column rule: all cells styled when age > 50 AND name contains "o"
  {
    apply: {
      color: { value: '#fef3c7' },
      textColor: { value: '#000000' },
    },
    column: null,
    priority: 0, // Lower priority so other rules take precedence
    where: {
      and: [
        { greaterThan: [{ column: 'age' }, 50] },
        { contains: [{ column: 'name' }, 'o'] },
      ],
    },
  },
];

const Component = () => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const columns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      header: () => 'Name',
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      header: () => 'Age',
      minSize: 80,
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      header: () => 'Cypher Command',
      minSize: 400,
      size: 400,
    }),
  ];

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns,
    data: TABLE_DEMO_DATA,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: true,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    initialState: {
      pagination: {
        pageSize: 10,
      },
    },
  });

  return (
    <div className="n-w-full n-bg-light-neutral-text-weakest">
      <DataGrid
        isResizable={false}
        tableInstance={table}
        isKeyboardNavigable={false}
        styleRules={styleRules}
        styling={{
          borderStyle: 'all-sides',
          hasHoverEffects: true,
          hasZebraStriping: false,
        }}
      />
    </div>
  );
};

export default Component;
```

### Datagrid Rule Based Styling Datetime

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from '@tanstack/react-table';

import { type DataGridStyleRule } from '../style-rules/types';

type EventRow = {
  event: string;
  /**
   * JS Date coerced from the event's local ISO string. Natively converted to
   * Unix ms by the engine and compared against { datetime: "..." } literals.
   */
  scheduledDate: Date;
  /**
   * UTC milliseconds since midnight. Compared against { time: "..." } literals
   * that are defined in Stockholm / CEST (+02:00) time — the engine normalises
   * both sides to UTC before comparing.
   */
  scheduledTimeMs: number;
  /** Human-readable local time shown in the cell, e.g. "09:00 CEST (+02:00)". */
  localTimeDisplay: string;
  status: 'confirmed' | 'delayed' | 'cancelled';
};

const data: EventRow[] = [
  // ── Past events (before start of 2026-06-25 in Stockholm / CEST) ──────────
  {
    event: 'Quarterly review',
    localTimeDisplay: '09:00 CEST (+02:00)',
    // 09:00 Stockholm CEST = 07:00 UTC
    scheduledDate: new Date('2026-06-10T09:00:00+02:00'),
    scheduledTimeMs: 7 * 3_600_000,
    status: 'confirmed',
  },
  {
    event: 'Security audit',
    localTimeDisplay: '07:30 BST (+01:00)',
    // 07:30 London BST (+01:00) = 06:30 UTC — before Stockholm opens
    scheduledDate: new Date('2026-06-18T07:30:00+01:00'),
    scheduledTimeMs: 6 * 3_600_000 + 30 * 60_000,
    status: 'delayed',
  },
  // ── Today (2026-06-25 in Stockholm / CEST) ─────────────────────────────────
  {
    event: 'Customer demo',
    localTimeDisplay: '09:00 EDT (-04:00)',
    // 09:00 New York EDT (-04:00) = 13:00 UTC
    scheduledDate: new Date('2026-06-25T09:00:00-04:00'),
    scheduledTimeMs: 13 * 3_600_000,
    status: 'confirmed',
  },
  {
    event: 'Platform migration',
    localTimeDisplay: '22:00 CEST (+02:00)',
    // 22:00 Stockholm CEST (+02:00) = 20:00 UTC — after Stockholm closes
    scheduledDate: new Date('2026-06-25T22:00:00+02:00'),
    scheduledTimeMs: 20 * 3_600_000,
    status: 'confirmed',
  },
  // ── Near future (up to and including 2026-07-14 in Stockholm) ─────────────
  {
    event: 'Board presentation',
    localTimeDisplay: '10:00 SGT (+08:00)',
    // 10:00 Singapore SGT (+08:00) = 02:00 UTC — before Stockholm opens
    scheduledDate: new Date('2026-07-02T10:00:00+08:00'),
    scheduledTimeMs: 2 * 3_600_000,
    status: 'confirmed',
  },
  {
    event: 'Partner onboarding',
    localTimeDisplay: '13:15 CEST (+02:00)',
    // 13:15 Paris CEST (+02:00) = 11:15 UTC
    scheduledDate: new Date('2026-07-08T13:15:00+02:00'),
    scheduledTimeMs: 11 * 3_600_000 + 15 * 60_000,
    status: 'cancelled',
  },
  // ── Far future (after 2026-07-15T00:00 Stockholm / CEST = 2026-07-14T22:00Z)
  {
    event: 'Architecture summit',
    localTimeDisplay: '08:00 CEST (+02:00)',
    // 08:00 Amsterdam CEST (+02:00) = 06:00 UTC.
    // Although the calendar date is "July 15", 08:00 CEST is still within the
    // same UTC day as July 14 22:00Z, so it crosses the far-future boundary.
    scheduledDate: new Date('2026-07-15T08:00:00+02:00'),
    scheduledTimeMs: 6 * 3_600_000,
    status: 'confirmed',
  },
  {
    event: 'Annual conference',
    localTimeDisplay: '09:00 PDT (-07:00)',
    // 09:00 San Francisco PDT (-07:00) = 16:00 UTC — after Stockholm closes
    scheduledDate: new Date('2026-08-03T09:00:00-07:00'),
    scheduledTimeMs: 16 * 3_600_000,
    status: 'confirmed',
  },
  {
    event: 'Year-end planning',
    localTimeDisplay: '09:00 JST (+09:00)',
    // 09:00 Tokyo JST (+09:00) = 00:00 UTC — midnight, well before Stockholm opens
    scheduledDate: new Date('2026-11-20T09:00:00+09:00'),
    scheduledTimeMs: 0,
    status: 'confirmed',
  },
];

/**
 * All date and time boundaries are anchored to Stockholm in June–July 2026.
 * Date literals use RFC 9557 IANA bracket notation — the UTC offset is inferred
 * from the named zone (CEST = +02:00 in this period); writing `+02:00` explicitly
 * is optional. Time literals use UTC-offset notation.
 *
 * "Today" from a Stockholm perspective:
 *   2026-06-25T00:00:00[Europe/Stockholm] = 2026-06-24T22:00:00Z
 *   2026-06-26T00:00:00[Europe/Stockholm] = 2026-06-25T22:00:00Z
 *
 * "Far future" threshold:
 *   2026-07-15T00:00:00[Europe/Stockholm] = 2026-07-14T22:00:00Z
 *
 * Business hours (Stockholm):
 *   { time: '09:00:00+02:00' } resolves to 07:00 UTC (25 200 000 ms)
 *   { time: '17:00:00+02:00' } resolves to 15:00 UTC (54 000 000 ms)
 */
const styleRules: DataGridStyleRule[] = [
  // Past events — entire row in muted red
  {
    apply: {
      color: { value: '#fee2e2' },
      textColor: { value: '#7f1d1d' },
    },
    column: null,
    priority: 1,
    where: {
      lessThan: [
        { column: 'scheduledDate' },
        { datetime: '2026-06-25T00:00:00[Europe/Stockholm]' },
      ],
    },
  },
  // Today (in Stockholm timezone) — entire row in amber
  {
    apply: {
      color: { value: '#fef9c3' },
      textColor: { value: '#713f12' },
    },
    column: null,
    priority: 1,
    where: {
      and: [
        {
          greaterThanOrEqual: [
            { column: 'scheduledDate' },
            { datetime: '2026-06-25T00:00:00[Europe/Stockholm]' },
          ],
        },
        {
          lessThan: [
            { column: 'scheduledDate' },
            { datetime: '2026-06-26T00:00:00[Europe/Stockholm]' },
          ],
        },
      ],
    },
  },
  // Far future (after 2026-07-15 midnight Stockholm time) — entire row in green
  {
    apply: {
      color: { value: '#dcfce7' },
      textColor: { value: '#14532d' },
    },
    column: null,
    priority: 1,
    where: {
      greaterThanOrEqual: [
        { column: 'scheduledDate' },
        { datetime: '2026-07-15T00:00:00[Europe/Stockholm]' },
      ],
    },
  },
  // Outside Stockholm business hours (before 09:00 CEST or from 17:00 CEST) —
  // time cell in orange. The { time: "..." } literals carry an offset so the
  // engine normalises them to UTC before comparing against scheduledTimeMs.
  {
    apply: {
      color: { value: '#ffedd5' },
      textColor: { value: '#7c2d12' },
    },
    column: 'scheduledTimeMs',
    priority: 2,
    where: {
      or: [
        {
          lessThan: [{ column: 'scheduledTimeMs' }, { time: '09:00:00+02:00' }],
        },
        {
          greaterThanOrEqual: [
            { column: 'scheduledTimeMs' },
            { time: '17:00:00+02:00' },
          ],
        },
      ],
    },
  },
  // Cancelled events — all cells grayed out at highest priority
  {
    apply: {
      color: { value: '#f3f4f6' },
      textColor: { value: '#9ca3af' },
    },
    column: null,
    priority: 5,
    where: { equal: [{ column: 'status' }, 'cancelled'] },
  },
];

const Component = () => {
  const columnHelper = createColumnHelper<EventRow>();

  const columns = [
    columnHelper.accessor('event', {
      cell: (info) => info.getValue(),
      header: () => 'Event',
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('scheduledDate', {
      cell: (info) =>
        info.getValue().toLocaleDateString('en-GB', {
          day: 'numeric',
          month: 'short',
          timeZone: 'Europe/Stockholm',
          year: 'numeric',
        }),
      header: () => 'Date (Stockholm)',
      minSize: 150,
      size: 170,
    }),
    columnHelper.accessor('localTimeDisplay', {
      cell: (info) => info.getValue(),
      header: () => 'Local time',
      minSize: 160,
      size: 180,
    }),
    columnHelper.accessor('scheduledTimeMs', {
      cell: (info) => {
        const ms = info.getValue();
        const h = Math.floor(ms / 3_600_000)
          .toString()
          .padStart(2, '0');
        const m = Math.floor((ms % 3_600_000) / 60_000)
          .toString()
          .padStart(2, '0');
        return `${h}:${m} UTC`;
      },
      header: () => 'Time (UTC)',
      minSize: 110,
      size: 130,
    }),
    columnHelper.accessor('status', {
      cell: (info) => info.getValue(),
      header: () => 'Status',
      minSize: 100,
      size: 120,
    }),
  ];

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns,
    data,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: true,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    initialState: {
      pagination: { pageSize: 10 },
    },
  });

  return (
    <div className="n-w-full n-bg-light-neutral-text-weakest">
      <DataGrid
        isResizable={false}
        isKeyboardNavigable={false}
        tableInstance={table}
        styleRules={styleRules}
        styling={{
          borderStyle: 'all-sides',
          hasHoverEffects: true,
          hasZebraStriping: false,
        }}
      />
    </div>
  );
};

export default Component;
```

### Datagrid Selectable

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { Checkbox, DataGrid } from '@neo4j-ndl/react';
import {
  type ColumnDef,
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  useReactTable,
} from '@tanstack/react-table';
import { useState } from 'react';

import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const selectableColumn: ColumnDef<TestDataFormat> = {
  cell: ({ row }) => (
    <div style={{ padding: '4px 0' }}>
      <Checkbox
        ariaLabel={`row-${row.index}`}
        isChecked={row.getIsSelected() || false}
        onChange={row.getToggleSelectedHandler()}
      />
    </div>
  ),
  header: ({ table }) => (
    <Checkbox
      ariaLabel="Select all rows"
      isChecked={table.getIsAllRowsSelected() || false}
      // If you want to select all pages from the current page use:
      // https://tanstack.com/table/v8/docs/api/features/row-selection#gettoggleallpagerowsselectedhandler
      onChange={table.getToggleAllRowsSelectedHandler()}
      isIndeterminate={table.getIsSomeRowsSelected()}
    />
  ),
  id: 'select',
  maxSize: 80,
  minSize: 48,
  size: 80,
};
const columnHelper = createColumnHelper<TestDataFormat>();

const commonColumns = [
  columnHelper.accessor('name', {
    cell: (info) => info.getValue(),
    footer: (info) => info.column.id,
    header: () => 'Name',
    maxSize: 300,
    minSize: 180,
    size: 220,
  }),
  columnHelper.accessor('age', {
    cell: (info) => info.renderValue(),
    footer: (info) => info.column.id,
    header: () => 'Age',
    minSize: 80,
  }),
  columnHelper.accessor('cypherCommand', {
    cell: (info) => info.getValue(),
    footer: (info) => info.column.id,
    header: () => 'Cypher Command',
    minSize: 400,
    size: 400,
  }),
];

const allColumns = [selectableColumn, ...commonColumns];

const Component = () => {
  const [rowSelection, setRowSelection] = useState({});

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns: allColumns,
    data: TABLE_DEMO_DATA,
    enableSorting: false,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    initialState: {
      pagination: {
        pageSize: 5,
      },
    },
    onRowSelectionChange: setRowSelection,
    state: {
      rowSelection,
    },
  });

  return (
    <DataGrid
      isResizable={false}
      tableInstance={table}
      isKeyboardNavigable={false}
    />
  );
};

export default Component;
```

### Datagrid Simple

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from '@tanstack/react-table';

import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const Component = () => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const columns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Name',
      maxSize: 300,
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      footer: (info) => info.column.id,
      header: () => 'Age',
      minSize: 80,
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Cypher Command',
      minSize: 400,
      size: 400,
    }),
  ];

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns,
    data: TABLE_DEMO_DATA,
    debugColumns: true,
    debugHeaders: true,
    debugTable: true,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: true,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    initialState: {
      pagination: {
        pageSize: 5,
      },
    },
  });

  return (
    <div className="n-w-full n-bg-light-neutral-text-weakest">
      <DataGrid
        isResizable={false}
        tableInstance={table}
        isKeyboardNavigable={false}
        styling={{
          borderStyle: 'all-sides',
          hasHoverEffects: true,
          hasZebraStriping: false,
        }}
      />
    </div>
  );
};

export default Component;
```

### Datagrid Single Selectable

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid, Radio, useDataGridContext } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  useReactTable,
} from '@tanstack/react-table';

type PricingEntry = {
  cpuCount: number;
  id: number;
  memory: string;
  storage: string;
};

const columnHelper = createColumnHelper<PricingEntry>();

const DEMO_PRICING: PricingEntry[] = [
  {
    cpuCount: 1,
    id: 1,
    memory: '1 GB',
    storage: '10 GB',
  },
  {
    cpuCount: 2,
    id: 2,
    memory: '2 GB',
    storage: '20 GB',
  },
  {
    cpuCount: 3,
    id: 3,
    memory: '3 GB',
    storage: '30 GB',
  },
];

const columns = [
  columnHelper.accessor('id', {
    cell: ({ cell, row }) => (
      <Radio
        ariaLabel={`value-${cell.getValue()}`}
        isChecked={row.getIsSelected()}
      />
    ),
    header: () => null,
    maxSize: 50,
    size: 50,
  }),
  columnHelper.accessor('memory', {
    cell: (info) => info.getValue(),
    minSize: 120,
  }),
  columnHelper.accessor('cpuCount', {
    cell: (info) => info.getValue(),
  }),
  columnHelper.accessor('storage', {
    cell: (info) => info.getValue(),
  }),
];

const CustomBodyRow = ({
  row,
}: React.ComponentProps<typeof DataGrid.BodyRow<PricingEntry>>) => {
  const { components } = useDataGridContext<PricingEntry>();

  return (
    <div key={row.id} role="row" className="ndl-data-grid-tr">
      {row.getVisibleCells().map(
        (cell) =>
          components.BodyCell && (
            <components.BodyCell
              key={cell.id}
              cell={cell}
              htmlAttributes={{
                onClick: () => row.toggleSelected(!row.getIsSelected()),
              }}
            />
          ),
      )}
    </div>
  );
};

const Component = () => {
  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns,
    data: DEMO_PRICING,
    enableMultiRowSelection: false,
    enableSorting: false,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    initialState: {
      rowSelection: {
        0: true,
      },
    },
  });

  return (
    <div className="n-w-full">
      <DataGrid
        components={{
          BodyRow: CustomBodyRow,
          Navigation: null,
        }}
        isKeyboardNavigable={false}
        isResizable={false}
        tableInstance={table}
      />
    </div>
  );
};

export default Component;
```

### Datagrid Skeleton Loading

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  useReactTable,
} from '@tanstack/react-table';

import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const Component = () => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const columns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Name',
      maxSize: 300,
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      footer: (info) => info.column.id,
      header: () => 'Age',
      minSize: 80,
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Cypher Command',
      minSize: 400,
      size: 400,
    }),
  ];

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns,
    data: TABLE_DEMO_DATA,
    debugColumns: true,
    debugHeaders: true,
    debugTable: true,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: false,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    initialState: {
      pagination: {
        pageSize: 5,
      },
    },
  });

  return (
    <div className="n-w-full n-bg-light-neutral-text-weakest">
      <DataGrid
        isResizable={true}
        tableInstance={table}
        isKeyboardNavigable={true}
        isSkeletonLoading={true}
      />
    </div>
  );
};

export default Component;
```

### Datagrid Sortable

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from '@tanstack/react-table';

import { TABLE_DEMO_DATA } from './staticData';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const Component = () => {
  const columnHelper = createColumnHelper<TestDataFormat>();

  const columns = [
    columnHelper.accessor('name', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Name',
      maxSize: 300,
      minSize: 180,
      size: 220,
    }),
    columnHelper.accessor('age', {
      cell: (info) => info.renderValue(),
      footer: (info) => info.column.id,
      header: () => 'Age',
      minSize: 80,
    }),
    columnHelper.accessor('cypherCommand', {
      cell: (info) => info.getValue(),
      footer: (info) => info.column.id,
      header: () => 'Cypher Command',
      minSize: 400,
      size: 400,
    }),
  ];

  const table = useReactTable({
    columnResizeMode: 'onChange',
    columns,
    data: TABLE_DEMO_DATA,
    debugColumns: true,
    debugHeaders: true,
    debugTable: true,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: true,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    initialState: {
      pagination: {
        pageSize: 5,
      },
    },
  });

  return (
    <div className="n-w-full n-bg-light-neutral-text-weakest">
      <DataGrid
        isResizable={true}
        tableInstance={table}
        isKeyboardNavigable={true}
      />
    </div>
  );
};

export default Component;
```

### Datagrid Using Editable Cells

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { tokens } from '@neo4j-ndl/base';
import { DataGrid, GraphLabel } from '@neo4j-ndl/react';
import { ExclamationTriangleIconSolid } from '@neo4j-ndl/react/icons';
import {
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from '@tanstack/react-table';
import { useState } from 'react';
import { type MultiValueProps } from 'react-select';

type TestDataFormat = {
  name: string;
  role: string;
  permissions: string[];
  labels: string[];
  status: string;
};

const ROLE_OPTIONS = [
  { label: 'Admin', value: 'admin' },
  { label: 'Editor', value: 'editor' },
  { label: 'Viewer', value: 'viewer' },
];

const PERMISSION_OPTIONS = [
  { label: 'Read', value: 'read' },
  { label: 'Write', value: 'write' },
  { label: 'Delete', value: 'delete' },
];

const LABEL_OPTIONS = [
  { label: 'Person', value: 'person' },
  { label: 'Movie', value: 'movie' },
  { label: 'Director', value: 'director' },
  { label: 'Actor', value: 'actor' },
];

const STATUS_OPTIONS = [
  { label: 'Active', value: 'active' },
  { label: 'Inactive', value: 'inactive' },
  { label: 'Pending', value: 'pending' },
];

const VALID_STATUSES = STATUS_OPTIONS.map((option) => option.value);

type DropDownOption = { value: string; label: string };

const LABEL_COLORS: Record<string, string> = {
  actor: tokens.graph['7'],
  director: tokens.graph['4'],
  movie: tokens.graph['3'],
  person: tokens.graph['1'],
};

// Render each selected value as a non-interactive GraphLabel (as a span)
// instead of the default Tag chip, demonstrating the components override.
const LabelMultiValue = (props: MultiValueProps<DropDownOption, boolean>) => (
  <GraphLabel
    as="span"
    type="node"
    size="small"
    color={LABEL_COLORS[props.data.value]}
  >
    {props.data.label}
  </GraphLabel>
);

// The "Dallas Kuhn" row has a status that is not part of the available
// options, so its dropdown cell renders in the error state.
const DEMO_DATA: TestDataFormat[] = [
  {
    labels: ['person', 'actor'],
    name: 'Alicia Sanford',
    permissions: ['read', 'write'],
    role: 'admin',
    status: 'active',
  },
  {
    labels: ['movie'],
    name: 'Dallas Kuhn',
    permissions: ['read'],
    role: 'editor',
    status: 'unknown',
  },
  {
    labels: ['director'],
    name: 'Susan Daugherty MD',
    permissions: ['read', 'write', 'delete'],
    role: 'viewer',
    status: 'pending',
  },
  {
    labels: ['person'],
    name: 'Karen Huel',
    permissions: ['read'],
    role: 'editor',
    status: 'inactive',
  },
  {
    labels: ['movie', 'director'],
    name: 'Ken Kessler',
    permissions: ['write', 'delete'],
    role: 'admin',
    status: 'active',
  },
  {
    labels: [],
    name: 'Courtney McClure',
    permissions: [],
    role: 'viewer',
    status: 'pending',
  },
];

const columnHelper = createColumnHelper<TestDataFormat>();

const Component = () => {
  const [data, setData] = useState(DEMO_DATA);

  const updateRow = (name: string, update: Partial<TestDataFormat>): void => {
    setData((oldData) =>
      oldData.map((row) => (row.name === name ? { ...row, ...update } : row)),
    );
  };

  const COMMON_COLUMNS = [
    // Editable inline text cell.
    columnHelper.accessor('name', {
      cell: (cell) => (
        <DataGrid.InlineEditCell
          cell={cell}
          value={cell.getValue()}
          ariaLabel="edit name"
        />
      ),
      meta: {
        isInlineEditCell: {
          onEditChange: (newValue, cell) => {
            updateRow(cell.row.original.name, { name: newValue });
          },
        },
      },
      minSize: 180,
      size: 220,
    }),
    // Default single-select dropdown.
    columnHelper.accessor('role', {
      cell: (info) => (
        <DataGrid.DropDownCell
          cell={info}
          options={ROLE_OPTIONS}
          ariaLabel="select role"
        />
      ),
      meta: {
        isDropDownCell: {
          onChange: (newValue, cell) => {
            if (newValue === undefined) {
              return;
            }
            updateRow(cell.row.original.name, { role: newValue });
          },
        },
      },
      minSize: 160,
      size: 180,
    }),
    // Default multi-select dropdown (default Tag chips).
    columnHelper.accessor('permissions', {
      cell: (info) => (
        <DataGrid.DropDownCell
          cell={info}
          options={PERMISSION_OPTIONS}
          ariaLabel="select permissions"
        />
      ),
      enableSorting: false,
      meta: {
        isDropDownCell: {
          isMulti: true,
          onChange: (newValue, cell) => {
            updateRow(cell.row.original.name, { permissions: newValue });
          },
        },
      },
      minSize: 220,
      size: 260,
    }),
    // Multi-select dropdown with a custom MultiValue (GraphLabel chips).
    columnHelper.accessor('labels', {
      cell: (info) => (
        <DataGrid.DropDownCell
          cell={info}
          options={LABEL_OPTIONS}
          ariaLabel="select labels"
          components={{ MultiValue: LabelMultiValue }}
        />
      ),
      enableSorting: false,
      meta: {
        isDropDownCell: {
          isMulti: true,
          onChange: (newValue, cell) => {
            updateRow(cell.row.original.name, { labels: newValue });
          },
        },
      },
      minSize: 240,
      size: 280,
    }),
    // Single-select dropdown in an error state.
    columnHelper.accessor('status', {
      cell: (info) => {
        const isInvalid = !VALID_STATUSES.includes(info.getValue());

        const isPending = info.getValue() === 'pending';
        const isInactive = info.getValue() === 'inactive';

        const warningHelpText = (
          <span className="n-flex n-items-center n-gap-token-2 n-text-warning-text">
            <ExclamationTriangleIconSolid className="n-size-token-16 n-text-warning-icon" />
            Inactive users cannot sign in
          </span>
        );

        return (
          <DataGrid.DropDownCell
            cell={info}
            options={STATUS_OPTIONS}
            ariaLabel="select status"
            errorText={isInvalid ? 'Please select a valid status' : undefined}
            helpText={
              isInactive
                ? warningHelpText
                : isPending
                  ? 'Waiting for admin approval'
                  : undefined
            }
          />
        );
      },
      meta: {
        isDropDownCell: {
          onChange: (newValue, cell) => {
            if (newValue === undefined) {
              return;
            }
            updateRow(cell.row.original.name, { status: newValue });
          },
        },
      },
      minSize: 200,
      size: 240,
    }),
  ];
  const table = useReactTable({
    autoResetPageIndex: false,
    columnResizeMode: 'onChange',
    columns: COMMON_COLUMNS,
    data: data,
    defaultColumn: {
      maxSize: 800,
      minSize: 80,
    },
    enableSorting: true,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    initialState: {
      pagination: {
        pageSize: 5,
      },
    },
  });

  return (
    <div className="n-w-full n-bg-light-neutral-text-weakest">
      <DataGrid
        isKeyboardNavigable={true}
        isResizable={true}
        styling={{
          borderStyle: 'all-sides',
          hasZebraStriping: false,
        }}
        tableInstance={table}
      />
    </div>
  );
};

export default Component;
```

### Datagrid Virtualization

```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';

import { DataGrid } from '@neo4j-ndl/react';
import {
  createColumnHelper,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  type Table as DataGridType,
  useReactTable,
} from '@tanstack/react-table';
import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual';
import { type RefObject, useMemo, useRef } from 'react';

import { type CommonProps } from '../../_common/types';

type TestDataFormat = {
  name: string;
  age: number;
  cypherCommand: string;
};

const TABLE_DEMO_DATA: TestDataFormat[] = Array.from(
  { length: 1000 },
  (_, i) => ({
    age: Math.floor(Math.random() * 100),
    cypherCommand: `MATCH (n) RETURN n LIMIT ${i + 1}`,
    name: `Person ${i + 1}`,
  }),
);

const BodyCurryingFunction =
  (
    rowVirtualizer: Virtualizer<HTMLDivElement, Element>,
    ref: RefObject<HTMLDivElement | null>,
    table: DataGridType<TestDataFormat>,
  ) =>
  // eslint-disable-next-line react/display-name
  ({
    style,
    className,
    ...props
  }: CommonProps<'div', { children?: React.ReactNode }>) => {
    const { rows } = table.getSortedRowModel();
    rowVirtualizer.setOptions({
      ...rowVirtualizer.options,
      count: rows.length,
    });
    const items = rowVirtualizer.getVirtualItems();

    return (
      <div ref={ref} className="n-overflow-auto n-h-[290px]">
        <DataGrid.Body
          {...props}
          className={className}
          style={{
            ...style,
            height: `${rowVirtualizer.getTotalSize()}px`,
            position: 'relative',
          }}
        >
          {items.map((row: { index: number; size: number; start: number }) => {
            const foundRow = rows[row.index];
            if (foundRow === undefined) {
              return null;
            }

            return (
              <DataGrid.BodyRow
                key={`table-row-${row.index}`}
                row={foundRow}
                style={{
                  height: `${row.size}px`,
                  left: 0,
                  position: 'absolute',
                  top: 0,
                  transform: `translateY(${row.start}px)`,
                  width: '100%',
                }}
              />
            );
          })}
        </DataGrid.Body>
      </div>
    );
  };

/** Repeats array `n` times */
const repeat = (arr: unknown[], n: number) => Array(n).fill(arr).flat();

const columnHelper = createColumnHelper<TestDataFormat>();

const columns = [
  columnHelper.accessor('name', {
    cell: (info) => info.getValue(),
    footer: (info) => info.column.id,
    header: () => 'Name',
    maxSize: 300,
    minSize: 180,
    size: 220,
  }),
  columnHelper.accessor('age', {
    cell: (info) => info.renderValue(),
    footer: (info) => info.column.id,
    header: () => 'Age',
    minSize: 80,
  }),
  columnHelper.accessor('cypherCommand', {
    cell: (info) => info.getValue(),
    footer: (info) => info.column.id,
    header: () => 'Cypher Command',
    minSize: 400,
    size: 400,
  }),
];

const ExtendedProps = {
  columnResizeMode: 'onChange' as const,
  columns,
  data: [...repeat(TABLE_DEMO_DATA, 500)],
  debugColumns: true,
  debugHeaders: true,
  debugTable: true,
  defaultColumn: {
    maxSize: 800,
    minSize: 80,
  },
  enableSorting: false,
  getCoreRowModel: getCoreRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
};

const Component = () => {
  const scrollingParentRef = useRef<HTMLDivElement | null>(null);

  const rowVirtualizer = useVirtualizer({
    count: ExtendedProps.data.length,
    estimateSize: () => 40,
    getScrollElement: () => scrollingParentRef.current,
    // 40 is the size for a basic data type row so fallback to that
    overscan: 15,
  });

  const table = useReactTable({
    ...ExtendedProps,
    enableSorting: true,
    getSortedRowModel: getSortedRowModel(),
    initialState: {
      pagination: {
        pageSize: ExtendedProps.data.length,
      },
    },
  });

  const CustomBody = useMemo(() => {
    return BodyCurryingFunction(rowVirtualizer, scrollingParentRef, table);
  }, [rowVirtualizer, scrollingParentRef, table]);

  return (
    <div
      className="n-w-full n-bg-light-neutral-text-weakest"
      style={{ height: '400px' }}
    >
      <DataGrid
        components={{
          Body: CustomBody,
          PaginationNumericButton: null,
          RowsPerPage: null,
        }}
        isResizable={false}
        tableInstance={table}
        rootProps={{
          className: 'n-h-full',
        }}
      />
    </div>
  );
};

export default Component;
```
