Skip to content
Mendy UI

Table

Table

A virtualized data table with typed columns, selection, and column controls.

On this page
Project 1
Alex Morgan
$1,200.00
Project 2
Jordan Lee
$1,237.00
Project 3
Casey Taylor
$1,274.00
Project 4
Alex Morgan
$1,311.00
Project 5
Jordan Lee
$1,348.00
Project 6
Casey Taylor
$1,385.00
Project 7
Alex Morgan
$1,422.00
Project 8
Jordan Lee
$1,459.00
Project 9
Casey Taylor
$1,496.00
Project 10
Alex Morgan
$1,533.00
Project 11
Jordan Lee
$1,570.00
Project 12
Casey Taylor
$1,607.00
Project 13
Alex Morgan
$1,644.00
Project 14
Jordan Lee
$1,681.00
Project 15
Casey Taylor
$1,718.00
Project 16
Alex Morgan
$1,755.00
Project 17
Jordan Lee
$1,792.00
Project 18
Casey Taylor
$1,829.00
Project 19
Alex Morgan
$1,866.00
Project 20
Jordan Lee
$1,903.00

Sort a column, drag a resize handle, or select cells and copy them. Use the column settings to hide, reorder, and pin columns. The 10,000-row option uses the same component and API.

View source
"use client";
import { useMemo, useState } from "react";
import {
  DataTable,
  defineColumns,
  format,
  TableActionBar,
  useDataTable,
} from "@mendylanda/ui/table";
import { Button } from "@mendylanda/ui/primitives/button";

type Project = {
  id: string;
  name: string;
  owner: { name: string };
  budget: number;
  updatedAt: Date;
};
const columns = defineColumns<Project>((column) => [
  column.accessor("name", { label: "Project", pin: "start", size: 250 }),
  column.accessor("owner.name", { label: "Owner" }),
  column.accessor("budget", { label: "Budget", format: format.currency("USD") }),
  column.accessor("updatedAt", {
    label: "Updated",
    format: format.date({ dateStyle: "medium" }, "en-US"),
  }),
]);
export function TableDemo() {
  const [large, setLarge] = useState(false);
  const rows = useMemo(
    () =>
      Array.from({ length: large ? 10000 : 80 }, (_, index) => ({
        id: `project-${index}`,
        name: `Project ${index + 1}`,
        owner: { name: ["Alex Morgan", "Jordan Lee", "Casey Taylor"][index % 3] },
        budget: 1200 + index * 37,
        updatedAt: new Date(2026, 0, (index % 28) + 1),
      })),
    [large],
  );
  const table = useDataTable({
    rows,
    columns,
    getRowId: (row) => row.id,
    enableRowSelection: true,
    preferences: { key: "projects", scope: "docs-demo" },
  });
  return (
    <DataTable
      table={table}
      label="Projects"
      height={420}
      toolbar={
        <Button size="sm" variant="outline" onClick={() => setLarge(!large)}>
          {large ? "Show 80 rows" : "Try 10,000 rows"}
        </Button>
      }
      footer={
        <TableActionBar
          count={Object.keys(table.state.rowSelection).length}
          onClear={() => table.resetRowSelection(true)}
        >
          <span className="text-sm text-muted-foreground">
            Select cells and press ⌘C or Ctrl+C to copy.
          </span>
        </TableActionBar>
      }
    />
  );
}

Start with rows and columns

After installation, this is a complete table:

"use client";
import { DataTable, defineColumns, format } from "@mendylanda/ui/table";
 
type Project = { id: string; name: string; budget: number };
const rows: Project[] = [
  { id: "website", name: "Website", budget: 2400 },
  { id: "mobile", name: "Mobile app", budget: 5600 },
];
const columns = defineColumns<Project>((column) => [
  column.accessor("name", { label: "Project", pin: "start" }),
  column.accessor("budget", { label: "Budget", format: format.currency("USD", "en-US") }),
]);
 
export function ProjectsTable() {
  return <DataTable rows={rows} columns={columns} getRowId={(row) => row.id} label="Projects" />;
}

Use an ID that identifies the record across sorting, filtering, and page loads. An array index is not a stable record identity.

What comes by default

BehaviorDefault
RenderingVirtual rows and columns, sticky header, 44px rows.
ColumnsResizing, settings, visibility, reordering, pinning, and spare-width sharing.
Data processingClient sorting and filtering; pagination off.
Cell selectionDrag and keyboard selection, with spreadsheet-style copying.
Row selectionOpt in with enableRowSelection; checkboxes appear inside the first visible column.
HeightFit content, capped at 65dvh. Use layout="fill" for a full-page table.
AppearanceYour application's semantic colors, font, and radius.

Filters, pagination controls, row actions, and saved preferences are configured when needed. They are not required to render a table.

Connect your application

Use useDataTable when the query or other controls need access to state. Pass the resulting instance to <DataTable table={table} />. For a custom arrangement, compose TableView, TableFilters, TableColumnSettings, and other controls around that instance.

The API reference lists the props and exported functions.