Dice UI
Components

Data Grid

A high-performance editable data grid component with virtualization, keyboard navigation, and cell editing capabilities.

DocsAPI
"use client";
 
import { faker } from "@faker-js/faker";
import type { ColumnDef } from "@tanstack/react-table";
import * as React from "react";
import { DataGrid } from "@/components/data-grid/data-grid";
import { DataGridKeyboardShortcuts } from "@/components/data-grid/data-grid-keyboard-shortcuts";
import { useDataGrid } from "@/hooks/use-data-grid";
 
interface SkateTrick {
  id: string;
  trickName?: string;
  skaterName?: string;
  difficulty?: "beginner" | "intermediate" | "advanced" | "expert";
  variant?: "flip" | "grind" | "grab" | "transition" | "manual" | "slide";
  landed?: boolean;
  attempts?: number;
  bestScore?: number;
  location?: string;
  dateAttempted?: string;
}
 
const skateSpots = [
  "Venice Beach Skate Park",
  "Burnside Skate Park",
  "Love Park (Philadelphia)",
  "MACBA (Barcelona)",
  "Southbank (London)",
  "FDR Skate Park",
  "Brooklyn Banks",
  "El Toro High School",
  "Hubba Hideout",
  "Wallenberg High School",
  "EMB (Embarcadero)",
  "Pier 7 (San Francisco)",
] as const;
 
const skateTricks = {
  flip: [
    "Kickflip",
    "Heelflip",
    "Tre Flip",
    "Hardflip",
    "Inward Heelflip",
    "Frontside Flip",
    "Backside Flip",
    "Varial Flip",
    "Varial Heelflip",
    "Double Flip",
    "Laser Flip",
    "Anti-Casper Flip",
    "Casper Flip",
    "Impossible",
    "360 Flip",
    "Big Spin",
    "Bigspin Flip",
  ],
  grind: [
    "50-50 Grind",
    "5-0 Grind",
    "Nosegrind",
    "Crooked Grind",
    "Feeble Grind",
    "Smith Grind",
    "Lipslide",
    "Boardslide",
    "Tailslide",
    "Noseslide",
    "Bluntslide",
    "Nollie Backside Lipslide",
    "Switch Frontside Boardslide",
  ],
  grab: [
    "Indy Grab",
    "Melon Grab",
    "Stalefish",
    "Tail Grab",
    "Nose Grab",
    "Method",
    "Mute Grab",
    "Crail Grab",
    "Seatbelt Grab",
    "Roast Beef",
    "Chicken Wing",
    "Tweaked Indy",
    "Japan Air",
  ],
  transition: [
    "Frontside Air",
    "Backside Air",
    "McTwist",
    "540",
    "720",
    "900",
    "Frontside 180",
    "Backside 180",
    "Frontside 360",
    "Backside 360",
    "Alley-Oop",
    "Fakie",
    "Revert",
    "Carve",
    "Pump",
    "Drop In",
  ],
  manual: [
    "Manual",
    "Nose Manual",
    "Casper",
    "Rail Stand",
    "Pogo",
    "Handstand",
    "One Foot Manual",
    "Spacewalk",
    "Truckstand",
    "Primo",
  ],
  slide: [
    "Powerslide",
    "Bert Slide",
    "Coleman Slide",
    "Pendulum Slide",
    "Stand-up Slide",
    "Toeside Slide",
    "Heelside Slide",
  ],
} as const;
 
function generateTrickData(): SkateTrick[] {
  return Array.from({ length: 30 }, () => {
    const variant = faker.helpers.arrayElement(
      Object.keys(skateTricks) as Array<keyof typeof skateTricks>,
    );
    const trickName = faker.helpers.arrayElement(skateTricks[variant]);
    const skaterName = faker.person.fullName();
    const attempts = faker.number.int({ min: 1, max: 50 });
    const landed = faker.datatype.boolean(0.6);
 
    const getDifficulty = (trick: string): SkateTrick["difficulty"] => {
      const expertTricks = [
        "Tre Flip",
        "900",
        "McTwist",
        "Laser Flip",
        "Impossible",
      ];
      const advancedTricks = [
        "Hardflip",
        "720",
        "540",
        "Crooked Grind",
        "Switch Frontside Boardslide",
      ];
      const intermediateTricks = [
        "Kickflip",
        "Heelflip",
        "Frontside 180",
        "50-50 Grind",
        "Boardslide",
      ];
 
      if (expertTricks.some((t) => trick.includes(t))) return "expert";
      if (advancedTricks.some((t) => trick.includes(t))) return "advanced";
      if (intermediateTricks.some((t) => trick.includes(t)))
        return "intermediate";
      return "beginner";
    };
 
    const difficulty = getDifficulty(trickName);
 
    return {
      id: faker.string.nanoid(),
      trickName,
      skaterName,
      difficulty,
      variant,
      landed,
      attempts,
      bestScore: landed
        ? faker.number.int({ min: 6, max: 10 })
        : faker.number.int({ min: 1, max: 5 }),
      location: faker.helpers.arrayElement(skateSpots),
      dateAttempted:
        faker.date
          .between({
            from: new Date(2023, 0, 1),
            to: new Date(),
          })
          .toISOString()
          .split("T")[0] ?? "",
    };
  });
}
 
const initialData: SkateTrick[] = generateTrickData();
 
export function DataGridDemo() {
  const [data, setData] = React.useState<SkateTrick[]>(initialData);
 
  const columns = React.useMemo<ColumnDef<SkateTrick>[]>(
    () => [
      {
        id: "trickName",
        accessorKey: "trickName",
        header: "Trick name",
        meta: {
          cell: {
            variant: "short-text",
          },
        },
        minSize: 180,
      },
      {
        id: "skaterName",
        accessorKey: "skaterName",
        header: "Skater",
        meta: {
          cell: {
            variant: "short-text",
          },
        },
        minSize: 150,
      },
      {
        id: "difficulty",
        accessorKey: "difficulty",
        header: "Difficulty",
        meta: {
          cell: {
            variant: "select",
            options: [
              { label: "Beginner", value: "beginner" },
              { label: "Intermediate", value: "intermediate" },
              { label: "Advanced", value: "advanced" },
              { label: "Expert", value: "expert" },
            ],
          },
        },
        minSize: 120,
      },
      {
        id: "variant",
        accessorKey: "variant",
        header: "Category",
        meta: {
          cell: {
            variant: "select",
            options: [
              { label: "Flip", value: "flip" },
              { label: "Grind", value: "grind" },
              { label: "Grab", value: "grab" },
              { label: "Transition", value: "transition" },
              { label: "Manual", value: "manual" },
              { label: "Slide", value: "slide" },
            ],
          },
        },
        minSize: 120,
      },
      {
        id: "landed",
        accessorKey: "landed",
        header: "Landed",
        meta: {
          cell: {
            variant: "checkbox",
          },
        },
        minSize: 100,
      },
      {
        id: "attempts",
        accessorKey: "attempts",
        header: "Attempts",
        meta: {
          cell: {
            variant: "number",
            min: 1,
            max: 100,
          },
        },
        minSize: 100,
      },
      {
        id: "bestScore",
        accessorKey: "bestScore",
        header: "Score",
        meta: {
          cell: {
            variant: "number",
            min: 1,
            max: 10,
          },
        },
        minSize: 110,
      },
      {
        id: "location",
        accessorKey: "location",
        header: "Location",
        meta: {
          cell: {
            variant: "select",
            options: skateSpots.map((spot) => ({ label: spot, value: spot })),
          },
        },
        minSize: 180,
      },
      {
        id: "dateAttempted",
        accessorKey: "dateAttempted",
        header: "Attempted at",
        meta: {
          cell: {
            variant: "date",
          },
        },
        minSize: 130,
      },
    ],
    [],
  );
 
  const onRowAdd = React.useCallback(() => {
    setData((prev) => [...prev, { id: faker.string.nanoid() }]);
 
    return {
      rowIndex: data.length,
      columnId: "trickName",
    };
  }, [data.length]);
 
  const { table, ...dataGridProps } = useDataGrid({
    columns,
    data,
    onDataChange: setData,
    onRowAdd,
    enableSearch: true,
  });
 
  return (
    <>
      <DataGridKeyboardShortcuts enableSearch={!!dataGridProps.searchState} />
      <DataGrid {...dataGridProps} table={table} height={340} />
    </>
  );
}

Installation

Install the main component and dependencies:

npx [email protected] add "https://diceui.com/r/data-grid"

Install the DataGridSortMenu (optional):

npx [email protected] add "https://diceui.com/r/data-grid-sort-menu"

Install the DataGridRowHeightMenu (optional):

npx [email protected] add "https://diceui.com/r/data-grid-row-height-menu"

Install the DataGridViewMenu (optional):

npx [email protected] add "https://diceui.com/r/data-grid-view-menu"

Install the DataGridKeyboardShortcuts (optional):

npx [email protected] add "https://diceui.com/r/data-grid-keyboard-shortcuts"

Features

The Data Grid component provides a comprehensive spreadsheet-like experience with:

  • High Performance: Virtualized rows and columns for handling large datasets
  • Cell Editing: In-place editing with various cell types (text, number, select, date, etc.)
  • Cell Selection: Single and multi-cell selection
  • Cell Copying: Copy selected cells to clipboard
  • Keyboard Navigation: Full keyboard support with Excel-like shortcuts
  • Context Menu: Right-click actions for rows and cells
  • Sorting: Multi-column sorting with drag-and-drop reordering and ascending/descending controls
  • Search: Find and navigate to matching cells with keyboard shortcuts
  • Row Management: Add, delete, and reorder rows
  • Column Resizing: Adjustable column widths

Layout

Import the components and compose them together:

import { DataGrid } from "@/components/data-grid/data-grid";
import { DataGridSortMenu } from "@/components/data-grid/data-grid-sort-menu";
import { DataGridRowHeightMenu } from "@/components/data-grid/data-grid-row-height-menu";
import { DataGridViewMenu } from "@/components/data-grid/data-grid-view-menu";
import { DataGridKeyboardShortcuts } from "@/components/data-grid/data-grid-keyboard-shortcuts";
import { useDataGrid } from "@/hooks/use-data-grid";

const { table, ...dataGridProps } = useDataGrid({
  data,
  columns,
  onDataChange: setData,
});

<div className="flex flex-col gap-4">

  {/* Toolbar with menu components */}
  <div className="flex items-center gap-2 self-end">
    <DataGridSortMenu table={table} />
    <DataGridRowHeightMenu table={table} />
    <DataGridViewMenu table={table} />
  </div>

  {/* Keyboard shortcuts dialog (opens with Ctrl+/) */}
  <DataGridKeyboardShortcuts enableSearch={!!dataGridProps.searchState} />

  {/* Data grid */}
  <DataGrid table={table} {...dataGridProps} />
</div>

Cell Architecture

The Data Grid uses a three-layer cell composition pattern:

  1. DataGridCell: Routes to the appropriate cell variant based on the column's meta.cell.variant property
  2. Cell Variants: Implement specific editing UIs for different data types (text, number, select, etc.)
  3. DataGridCellWrapper: Provides common functionality for all cells (focus, selection, keyboard interactions)
// Cell composition flow
<DataGridCell cell={cell} table={table} />

<ShortTextCell {...props} />  // Based on variant

<DataGridCellWrapper {...props}>
  {/* Cell-specific content */}
</DataGridCellWrapper>

Each cell variant receives the same props and wraps its content in DataGridCellWrapper, which provides:

  • Focus management and visual focus ring
  • Selection state and highlighting
  • Search match highlighting
  • Click, double-click, and keyboard event management
  • Edit mode triggering (Enter, F2, Space, or typing)

Cell Types

The Data Grid supports various cell types for different data formats:

Short Text Cell

{
  id: "name",
  accessorKey: "name",
  header: "Name",
  meta: {
    label: "Name",
    cell: {
      variant: "short-text",
    },
  },
}

Long Text Cell

{
  id: "notes",
  accessorKey: "notes",
  header: "Notes",
  meta: {
    label: "Notes",
    cell: {
      variant: "long-text",
    },
  },
}

Number Cell

{
  id: "price",
  accessorKey: "price",
  header: "Price",
  meta: {
    label: "Price",
    cell: {
      variant: "number",
      min: 0,
      step: 0.01,
    },
  },
}

Select Cell

{
  id: "category",
  accessorKey: "category",
  header: "Category",
  meta: {
    label: "Category",
    cell: {
      variant: "select",
      options: [
        { label: "Electronics", value: "electronics" },
        { label: "Clothing", value: "clothing" },
        { label: "Books", value: "books" },
      ],
    },
  },
}

Multi-Select Cell

{
  id: "skills",
  accessorKey: "skills",
  header: "Skills",
  meta: {
    label: "Skills",
    cell: {
      variant: "multi-select",
      options: [
        { label: "JavaScript", value: "javascript" },
        { label: "TypeScript", value: "typescript" },
        { label: "React", value: "react" },
      ],
    },
  },
}

Date Cell

{
  id: "startDate",
  accessorKey: "startDate",
  header: "Start Date",
  meta: {
    label: "Start Date",
    cell: {
      variant: "date",
    },
  },
}

Checkbox Cell

{
  id: "isActive",
  accessorKey: "isActive",
  header: "Active",
  meta: {
    label: "Active",
    cell: {
      variant: "checkbox",
    },
  },
}

Usage

With Toolbar and Keyboard Shortcuts

import { DataGrid } from "@/components/data-grid/data-grid";
import { DataGridKeyboardShortcuts } from "@/components/data-grid/data-grid-keyboard-shortcuts";
import { DataGridSortMenu } from "@/components/data-grid/data-grid-sort-menu";
import { DataGridRowHeightMenu } from "@/components/data-grid/data-grid-row-height-menu";
import { DataGridViewMenu } from "@/components/data-grid/data-grid-view-menu";
import { useDataGrid } from "@/hooks/use-data-grid";

export default function MyDataGrid() {
  const { table, ...dataGridProps } = useDataGrid({
    data,
    columns,
    enableSearch: true,
  });

  return (
    <div className="flex flex-col gap-4">
      {/* Toolbar with menu components */}
      <div className="flex items-center gap-2 self-end">
        <DataGridSortMenu table={table} />
        <DataGridRowHeightMenu table={table} />
        <DataGridViewMenu table={table} />
      </div>
      
      {/* Keyboard shortcuts dialog (opens with Ctrl+/) */}
      <DataGridKeyboardShortcuts enableSearch={!!dataGridProps.searchState} />
      
      {/* Data grid */}
      <DataGrid table={table} {...dataGridProps} />
    </div>
  );
}

Row Selection

Add a selection column to enable row selection:

import { Checkbox } from "@/components/ui/checkbox";

const columns = [
  {
    id: "select",
    header: ({ table }) => (
      <Checkbox
        checked={
          table.getIsAllPageRowsSelected() ||
          (table.getIsSomePageRowsSelected() && "indeterminate")
        }
        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(value) => row.toggleSelected(!!value)}
      />
    ),
    size: 40,
    enableSorting: false,
    enableHiding: false,
  },
  // ... other columns
];

Row Management

Add and delete rows with callbacks:

const onRowAdd = React.useCallback(() => {
  const newRow = {
    id: `${Date.now()}`,
    // ... other default values
  };
  setData(prev => [...prev, newRow]);
  
  return {
    rowIndex: data.length,
    columnId: "name", // Focus this column after adding
  };
}, [data.length]);

const onRowsDelete = React.useCallback((rows) => {
  setData(prev => prev.filter(row => !rows.includes(row)));
}, []);

const { table, ...dataGridProps } = useDataGrid({
  data,
  columns,
  onRowAdd,
  onRowsDelete,
});

Context Menu Actions

Right-click on cells to access context menu options:

  • Copy: Copy selected cells to clipboard
  • Clear: Clear content from selected cells
  • Delete rows: Remove selected rows (only available when onRowsDelete is provided)

API Reference

useDataGrid

Hook for initializing the data grid with state management and editing capabilities.

Prop

Type

DataGrid

Main data grid with virtualization and editing capabilities.

Prop

Type

DataGridColumnHeader

Column header with sorting controls and visual indicators for sort direction.

Prop

Type

DataGridCell

Routes to the appropriate cell variant based on the column's meta.cell.variant property.

Prop

Type

DataGridCellWrapper

Base wrapper providing common functionality for all cell variants including focus management, selection state, search highlighting, and keyboard interactions.

Prop

Type

DataGridCellVariants

Individual cell variants for different data types. Each variant implements the DataGridCellVariantProps interface and wraps its content in DataGridCellWrapper.

Prop

Type

Available cell variants:

  • ShortTextCell: Single-line text input with inline contentEditable
  • LongTextCell: Multi-line textarea displayed in a popover dialog
  • NumberCell: Numeric input with optional min, max, and step constraints
  • SelectCell: Single-select dropdown with predefined options
  • MultiSelectCell: Multi-select input with badge display and command palette
  • CheckboxCell: Boolean checkbox for true/false values
  • DateCell: Date picker with calendar popover

Creating Custom Cell Variants

You can create custom cell variants by implementing the DataGridCellVariantProps interface and wrapping your content in DataGridCellWrapper:

import { DataGridCellWrapper } from "@/components/data-grid/data-grid-cell-wrapper";
import type { DataGridCellVariantProps } from "@/types/docs/data-grid";

export function CustomCell<TData>({
  cell,
  table,
  rowIndex,
  columnId,
  isFocused,
  isEditing,
  isSelected,
}: DataGridCellVariantProps<TData>) {
  const value = cell.getValue() as YourType;
  
  return (
    <DataGridCellWrapper
      cell={cell}
      table={table}
      rowIndex={rowIndex}
      columnId={columnId}
      isEditing={isEditing}
      isFocused={isFocused}
      isSelected={isSelected}
    >
      {/* Your custom cell content */}
    </DataGridCellWrapper>
  );
}

DataGridRow

Individual row with virtualization for large datasets.

Prop

Type

DataGridSearch

Search dialog with keyboard shortcuts for finding and navigating between matching cells in the grid.

Prop

Type

DataGridContextMenu

Right-click context menu for quick access to common cell and row actions like copy, clear, and delete.

Prop

Type

DataGridSortMenu

Menu for managing multi-column sorting with drag-and-drop reordering and ascending/descending controls.

Prop

Type

DataGridRowHeightMenu

Menu for adjusting row heights between short, medium, tall, and extra-tall options with persistent preferences.

Prop

Type

DataGridViewMenu

Menu for controlling column visibility with search and toggle all functionality.

Prop

Type

DataGridKeyboardShortcuts

Searchable reference dialog for all available keyboard shortcuts for navigating and interacting with the data grid.

Prop

Type

Accessibility

The Data Grid follows WAI-ARIA guidelines for grid widgets:

  • Full keyboard navigation support
  • Screen reader announcements for cell changes
  • Focus management during editing
  • Proper ARIA labels and roles
  • High contrast mode support

Keyboard Interactions

KeyDescription
Navigate between cells
TabMove to next cell
ShiftTabMove to previous cell
HomeMove to first column
EndMove to last column
Ctrl + HomeCmd + HomeMove to first cell
Ctrl + EndCmd + EndMove to last cell
PgUpMove up one page
PgDnMove down one page
Shift + ↑Shift + ↓Shift + ←Shift + →Extend selection
Ctrl + ACmd + ASelect all cells
Ctrl + ClickCmd + ClickToggle cell selection
Shift + ClickSelect range
EscapeClear selection or exit edit mode
EnterStart editing cell
Double ClickStart editing cell
DeleteClear selected cells
BackspaceClear selected cells
Ctrl + FCmd + FOpen search
Ctrl + Shift + SCmd + Shift + SToggle the sort menu
Ctrl + /Cmd + /Show keyboard shortcuts

Credits