> Leaf UI 0.8.1 · Package: @ifhover/leaf-ui · Source: 435bab23cad7d343653d9b63b049c0b0601eeefb
> Browser documentation: https://ifhover.github.io/leaf-ui/en/components/table.html

> For AI agents: the complete documentation index is available at https://ifhover.github.io/leaf-ui/en/llms.txt, the full documentation bundle is available at https://ifhover.github.io/leaf-ui/en/llms-full.txt.

# DataTable

Browse, filter and manage structured data.

## Installation

DataTable accepts data and columns for sorting and pagination, or your own TanStack Table instance for filtering, selection, expansion and column controls. Install the peer dependency before using it:

```bash
npm install @tanstack/react-table@^8.21.3
```

```tsx
import { DataTable } from '@ifhover/leaf-ui/table';
import type { DataTableProps, DataTableColumnMeta } from '@ifhover/leaf-ui/table';
```

The table subpath loads its styles automatically. Import DataTable and its data types from `@ifhover/leaf-ui/table`; DataTable is not exported from the library root.

## Basic usage


| Member | Email | Team | Status |
| --- | --- | --- | --- |
| Alex Morgan | alex@example.com | Design | Active |
| Sam Taylor | sam@example.com | Engineering | Active |
| Jordan Lee | jordan@example.com | Product | Invited |

basic.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/table/basic.tsx"
import { Tag } from '@ifhover/leaf-ui';

import { DataTable } from '@ifhover/leaf-ui/table';

const people = [
  { id: '1', name: 'Alex Morgan', email: 'alex@example.com', role: 'Design', active: true },
  { id: '2', name: 'Sam Taylor', email: 'sam@example.com', role: 'Engineering', active: true },
  { id: '3', name: 'Jordan Lee', email: 'jordan@example.com', role: 'Product', active: false },
];

export function TableBasic() {
  return (
    <DataTable
      data={people}
      columns={[
        { accessorKey: 'name', header: 'Member' },
        { accessorKey: 'email', header: 'Email' },
        { accessorKey: 'role', header: 'Team' },
        {
          accessorKey: 'active',
          header: 'Status',
          cell: ({ row }) => (
            <Tag color={row.original.active ? 'success' : 'default'}>
              {row.original.active ? ('Active') : 'Invited'}
            </Tag>
          ),
        },
      ]}
      rowKey="id"
      aria-label={'Team members'}
      minWidth={560}
      pagination={false}
    />
  );
}
```

Expand code

The `data + columns + rowKey` mode creates one TanStack instance and uses the same renderer as the advanced mode. `rowKey` must identify a stable, unique string/number field or return a string. Local sorting, filtering, pagination and expansion are configured by default. Use `options` for initial state, controlled callbacks or server configuration. `pagination={false}` displays all rows without internally truncating the data.

These modes are mutually exclusive: when supplying `table`, do not also pass `data`, `columns`, `rowKey` or `options`. Prefer your own `useReactTable` instance for advanced grouping or query state.

## Borders and striped rows

DataTable defaults to no outer frame or column dividers, with `striped` rows enabled. Borderless tables have square corners and no tinted header background, with a 1px divider below the header. Enable `bordered` for a rounded frame, white header background and column dividers (following the dark surface in dark mode). Headers are bold and slightly lighter than body text in both modes, with sorting icons immediately after the label. Stripes start on the first body row with a faint background and hide row dividers; set `striped={false}` to show fine dividers between rows. Both ordinary and striped rows show subtle primary-color feedback on hover.


BordersStriped rows

| Member | Team |
| --- | --- |
| Alex Morgan | Design |
| Sam Taylor | Engineering |
| Jordan Lee | Product |

appearance.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/table/appearance.tsx"
import { Checkbox, Space } from '@ifhover/leaf-ui';

import { DataTable } from '@ifhover/leaf-ui/table';

import { useState } from 'react';

const people = [
  { name: 'Alex Morgan', team: 'Design' },
  { name: 'Sam Taylor', team: 'Engineering' },
  { name: 'Jordan Lee', team: 'Product' },
];

export function TableAppearance() {
  const [bordered, setBordered] = useState(false);
  const [striped, setStriped] = useState(true);
  return (
    <Space direction="vertical" align="stretch">
      <Space wrap>
        <Checkbox checked={bordered} onChange={(event) => setBordered(event.target.checked)}>
          {'Borders'}
        </Checkbox>
        <Checkbox checked={striped} onChange={(event) => setStriped(event.target.checked)}>
          {'Striped rows'}
        </Checkbox>
      </Space>
      <DataTable
        data={people}
        columns={[
          { accessorKey: 'name', header: 'Member' },
          { accessorKey: 'team', header: 'Team' },
        ]}
        rowKey="name"
        pagination={false}
        aria-label={'Table appearance'}
        bordered={bordered}
        striped={striped}
      />
    </Space>
  );
}
```

Expand code

## Sorting, filtering, selection and pagination


0 selectedColumns

|  | Expand rows | Member | Email | Team | Projects | Status |
| --- | --- | --- | --- | --- | --- | --- |
|  |  | A**Alex Morgan** | member1@example.com | Design | 1 | Active |
|  |
|  |  | S**Sam Taylor** | member2@example.com | Engineering | 8 | Active |
|  |
|  |  | J**Jordan Lee** | member3@example.com | Product | 15 | Invited |
|  |
|  |  | R**Riley Chen** | member4@example.com | Design | 4 | Active |
|  |
|  |  | C**Casey Wilson** | member5@example.com | Engineering | 11 | Active |
|  |

26 members123456

data.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/table/data.tsx"
import { Avatar, Tag } from '@ifhover/leaf-ui';

import { DataTable } from '@ifhover/leaf-ui/table';

import { type ColumnDef, type ColumnFiltersState, getCoreRowModel, getExpandedRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, type PaginationState, type RowSelectionState, type SortingState, useReactTable } from '@tanstack/react-table';

import { useMemo, useState } from 'react';

export interface Member {
  id: string;
  name: string;
  email: string;
  team: string;
  projects: number;
  active: boolean;
}

export const members: Member[] = Array.from({ length: 26 }, (_, index) => ({
  id: `member-${index + 1}`,
  name:
    ['Alex Morgan', 'Sam Taylor', 'Jordan Lee', 'Riley Chen', 'Casey Wilson', 'Jamie Park'][
      index % 6
    ] ?? '',
  email: `member${index + 1}@example.com`,
  team: ['Design', 'Engineering', 'Product'][index % 3] ?? '',
  projects: ((index * 7) % 18) + 1,
  active: index % 4 !== 2,
}));

export function useMemberColumns(english = false) {
  return useMemo<ColumnDef<Member>[]>(
    () => [
      {
        accessorKey: 'name',
        header: english ? 'Member' : '成员',
        size: 230,
        minSize: 160,
        maxSize: 400,
        enableHiding: false,
        cell: ({ row }) => (
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <Avatar size={30}>{row.original.name.slice(0, 1)}</Avatar>
            <strong>{row.original.name}</strong>
          </div>
        ),
      },
      {
        accessorKey: 'email',
        header: english ? 'Email' : '邮箱',
        size: 250,
        minSize: 180,
        maxSize: 400,
      },
      {
        accessorKey: 'team',
        header: english ? 'Team' : '团队',
        size: 150,
        minSize: 100,
        maxSize: 240,
      },
      {
        accessorKey: 'projects',
        header: english ? 'Projects' : '项目数',
        size: 110,
        minSize: 90,
        maxSize: 160,
        meta: { align: 'end' },
      },
      {
        accessorKey: 'active',
        header: english ? 'Status' : '状态',
        size: 110,
        minSize: 100,
        maxSize: 160,
        cell: ({ getValue }) => (
          <Tag color={getValue() ? 'success' : 'default'}>
            {getValue() ? (english ? 'Active' : '活跃') : english ? 'Invited' : '已邀请'}
          </Tag>
        ),
      },
    ],
    [english],
  );
}

export function TableData() {
  const columns = useMemberColumns(true);
  const [sorting, setSorting] = useState<SortingState>([]);
  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
  const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 5 });
  const table = useReactTable({
    data: members,
    columns,
    getRowId: (member) => member.id,
    state: { sorting, rowSelection, columnFilters, pagination },
    onSortingChange: setSorting,
    onRowSelectionChange: setRowSelection,
    onColumnFiltersChange: setColumnFilters,
    onPaginationChange: setPagination,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getExpandedRowModel: getExpandedRowModel(),
    getRowCanExpand: () => true,
    enableRowSelection: (row) => row.original.active,
  });
  return (
    <DataTable
      table={table}
      aria-label={'Team directory'}
      selection="multiple"
      getRowLabel={(row) => `${row.original.name} (${row.original.id})`}
      renderExpandedRow={(row) => (
        <p style={{ margin: 0 }}>
          {'Contact'}: {row.original.email} · {'Team'}
          : {row.original.team}
        </p>
      )}
    >
      <DataTable.Toolbar>
        <DataTable.Search columnId="name" />
        <span style={{ marginInlineStart: 'auto', color: 'var(--leaf-color-text-muted)' }}>
          {Object.values(rowSelection).filter(Boolean).length} {'selected'}
        </span>
        <DataTable.ColumnVisibility />
      </DataTable.Toolbar>
      <DataTable.Content />
      <DataTable.Footer>
        <DataTable.Pagination
          pageSizeOptions={[5, 10, 20]}
          showTotal={(total) => (`${total} members`)}
        />
      </DataTable.Footer>
    </DataTable>
  );
}
```

Expand code

The provided instance is the **only state owner**. Leaf UI does not sort or paginate the data again, or create a second selection/filter store. Configure the row models required by your application:

| Feature              | TanStack configuration                                    |
| -------------------- | --------------------------------------------------------- |
| Data                 | `getCoreRowModel()`                                       |
| Client sorting       | `getSortedRowModel()`                                     |
| Client filtering     | `getFilteredRowModel()`                                   |
| Client pagination    | `getPaginationRowModel()`                                 |
| Tree/group expansion | `getExpandedRowModel()` and `getSubRows` for trees        |
| Detail rows          | `getRowCanExpand` and DataTable `renderExpandedRow`       |
| Grouping and totals  | `getGroupedRowModel()`, `aggregationFn`, `aggregatedCell` |

Provide a stable `getRowId`; do not use an index after sorting or pagination. Select-all defaults to selectable rows on the current page. `selectAllMode="all"` selects loaded, filtered records, **not unloaded server records**. Selection keys persist across pages in TanStack state; for server data use these keys rather than assuming `getSelectedRowModel()` contains unloaded records.

DataTable.Search updates the global filter unless columnId is specified. ColumnVisibility respects `enableHiding: false`. Shift-click a header for multi-sort; aria-sort identifies the primary sort column.

## Sticky headers, pinned columns and resizing


Drag a header edge or focus it and use the arrow keys.Reset widths

| Member--- | Email--- | Team--- | Projects--- | Status--- |
| --- | --- | --- | --- | --- |
| A**Alex Morgan** | member1@example.com | Design | 1 | Active |
| S**Sam Taylor** | member2@example.com | Engineering | 8 | Active |
| J**Jordan Lee** | member3@example.com | Product | 15 | Invited |
| R**Riley Chen** | member4@example.com | Design | 4 | Active |
| C**Casey Wilson** | member5@example.com | Engineering | 11 | Active |
| J**Jamie Park** | member6@example.com | Product | 18 | Active |
| A**Alex Morgan** | member7@example.com | Design | 7 | Invited |
| S**Sam Taylor** | member8@example.com | Engineering | 14 | Active |
| J**Jordan Lee** | member9@example.com | Product | 3 | Active |
| R**Riley Chen** | member10@example.com | Design | 10 | Active |
| C**Casey Wilson** | member11@example.com | Engineering | 17 | Invited |
| J**Jamie Park** | member12@example.com | Product | 6 | Active |
| A**Alex Morgan** | member13@example.com | Design | 13 | Active |
| S**Sam Taylor** | member14@example.com | Engineering | 2 | Active |
| J**Jordan Lee** | member15@example.com | Product | 9 | Invited |
| R**Riley Chen** | member16@example.com | Design | 16 | Active |
| C**Casey Wilson** | member17@example.com | Engineering | 5 | Active |
| J**Jamie Park** | member18@example.com | Product | 12 | Active |
| A**Alex Morgan** | member19@example.com | Design | 1 | Invited |
| S**Sam Taylor** | member20@example.com | Engineering | 8 | Active |
| J**Jordan Lee** | member21@example.com | Product | 15 | Active |
| R**Riley Chen** | member22@example.com | Design | 4 | Active |
| C**Casey Wilson** | member23@example.com | Engineering | 11 | Invited |
| J**Jamie Park** | member24@example.com | Product | 18 | Active |
| A**Alex Morgan** | member25@example.com | Design | 7 | Active |
| S**Sam Taylor** | member26@example.com | Engineering | 14 | Active |

data.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/table/data.tsx"
import { Avatar, Button, ConfigProvider, Tag } from '@ifhover/leaf-ui';

import { DataTable } from '@ifhover/leaf-ui/table';

import { type ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';

import { useMemo } from 'react';

export interface Member {
  id: string;
  name: string;
  email: string;
  team: string;
  projects: number;
  active: boolean;
}

export const members: Member[] = Array.from({ length: 26 }, (_, index) => ({
  id: `member-${index + 1}`,
  name:
    ['Alex Morgan', 'Sam Taylor', 'Jordan Lee', 'Riley Chen', 'Casey Wilson', 'Jamie Park'][
      index % 6
    ] ?? '',
  email: `member${index + 1}@example.com`,
  team: ['Design', 'Engineering', 'Product'][index % 3] ?? '',
  projects: ((index * 7) % 18) + 1,
  active: index % 4 !== 2,
}));

export function useMemberColumns(english = false) {
  return useMemo<ColumnDef<Member>[]>(
    () => [
      {
        accessorKey: 'name',
        header: english ? 'Member' : '成员',
        size: 230,
        minSize: 160,
        maxSize: 400,
        enableHiding: false,
        cell: ({ row }) => (
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <Avatar size={30}>{row.original.name.slice(0, 1)}</Avatar>
            <strong>{row.original.name}</strong>
          </div>
        ),
      },
      {
        accessorKey: 'email',
        header: english ? 'Email' : '邮箱',
        size: 250,
        minSize: 180,
        maxSize: 400,
      },
      {
        accessorKey: 'team',
        header: english ? 'Team' : '团队',
        size: 150,
        minSize: 100,
        maxSize: 240,
      },
      {
        accessorKey: 'projects',
        header: english ? 'Projects' : '项目数',
        size: 110,
        minSize: 90,
        maxSize: 160,
        meta: { align: 'end' },
      },
      {
        accessorKey: 'active',
        header: english ? 'Status' : '状态',
        size: 110,
        minSize: 100,
        maxSize: 160,
        cell: ({ getValue }) => (
          <Tag color={getValue() ? 'success' : 'default'}>
            {getValue() ? (english ? 'Active' : '活跃') : english ? 'Invited' : '已邀请'}
          </Tag>
        ),
      },
    ],
    [english],
  );
}

export function TableColumns() {
  const columns = useMemberColumns(true);
  const table = useReactTable({
    data: members,
    columns,
    getRowId: (member) => member.id,
    getCoreRowModel: getCoreRowModel(),
    columnResizeMode: 'onChange',
    initialState: { columnPinning: { left: ['name'], right: ['active'] } },
  });
  return (
    <ConfigProvider density="compact">
      <DataTable
        table={table}
        aria-label={'Pinned and resizable columns'}
        stickyHeader
        maxHeight={300}
        resizable
        pagination={false}
      >
        <DataTable.Toolbar>
          <span style={{ color: 'var(--leaf-color-text-muted)' }}>
            {'Drag a header edge or focus it and use the arrow keys.'}
          </span>
          <Button
            style={{ marginInlineStart: 'auto' }}
            size="sm"
            variant="soft"
            onClick={() => table.resetColumnSizing()}
          >
            {'Reset widths'}
          </Button>
        </DataTable.Toolbar>
        <DataTable.Content />
      </DataTable>
    </ConfigProvider>
  );
}
```

Expand code

Control pinning through `columnPinning`, order through `columnOrder`, and widths through each column's `size`, `minSize` and `maxSize`. Leaf interprets left/right as logical start/end sides. For RTL also configure TanStack `columnResizeDirection: 'rtl'`.

Tables fill their container and scroll horizontally only when the columns need more space. Headers are sticky by default. Set `maxHeight` to scroll overflowing rows within the body. Overlay tracks use [Scrollbar](https://ifhover.github.io/leaf-ui/en/components/scrollbar.md); the vertical track starts below the header. When content fits or reaches an edge, wheel scrolling continues to the page.

Drag a resize handle or focus it and press Left/Right. Shift increases the step; Home/End moves to its bounds; double-click resets its width. Set a sensible maxSize. ConfigProvider density and theme.components.Table customize spacing, radius, typography, foreground, background and shadow.

## Grouping and aggregation


| Expand rows | Team | Member | Projects |
| --- | --- | --- | --- |
|  | Design (3) |  | Total: 12 |
|  |  | Alex Morgan | 1 |
|  |  | Riley Chen | 4 |
|  |  | Alex Morgan | 7 |
|  | Engineering (3) |  | Total: 33 |
|  |  | Sam Taylor | 8 |
|  |  | Casey Wilson | 11 |
|  |  | Sam Taylor | 14 |
|  | Product (2) |  | Total: 33 |
|  |  | Jordan Lee | 15 |
|  |  | Jamie Park | 18 |

advanced.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/table/advanced.tsx"
import { Tag } from '@ifhover/leaf-ui';

import { DataTable } from '@ifhover/leaf-ui/table';

import { type ColumnDef, getCoreRowModel, getExpandedRowModel, getGroupedRowModel, useReactTable } from '@tanstack/react-table';

import { useMemo } from 'react';

import { type Member, members } from './data';

const groupedMembers = members.slice(0, 8);

export function TableGrouped() {
  const columns = useMemo<ColumnDef<Member>[]>(
    () => [
      { accessorKey: 'team', header: 'Team', size: 150 },
      { accessorKey: 'name', header: 'Member', size: 240 },
      {
        accessorKey: 'projects',
        header: 'Projects',
        aggregationFn: 'sum',
        meta: { align: 'end' },
        aggregatedCell: ({ getValue }) => (
          <Tag>
            {'Total'}: {String(getValue())}
          </Tag>
        ),
      },
    ],
    [true],
  );
  const table = useReactTable({
    data: groupedMembers,
    columns,
    getRowId: (member) => member.id,
    getCoreRowModel: getCoreRowModel(),
    getGroupedRowModel: getGroupedRowModel(),
    getExpandedRowModel: getExpandedRowModel(),
    initialState: { grouping: ['team'], expanded: true },
  });
  return (
    <DataTable
      table={table}
      aria-label={'Members grouped by team'}
      pagination={false}
    />
  );
}
```

Expand code

Ordinary grouped tables transition row height when expanding or collapsing, preserving child content and stripe positions. Regional motion settings and system reduced motion disable the transition; large or virtual tables also update immediately.

## Editable cells


| Member | Email | Team |
| --- | --- | --- |
|  | member1@example.com | Design |
|  | member2@example.com | Engineering |
|  | member3@example.com | Product |

advanced.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/table/advanced.tsx"
import { Input } from '@ifhover/leaf-ui';

import { DataTable } from '@ifhover/leaf-ui/table';

import { type ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';

import { useMemo, useState } from 'react';

import { type Member, members } from './data';

export function TableEditable() {
  const [data, setData] = useState(members.slice(0, 3));
  const columns = useMemo<ColumnDef<Member>[]>(
    () => [
      {
        accessorKey: 'name',
        header: 'Member',
        size: 240,
        cell: ({ row }) => (
          <Input
            aria-label={`${'Name'}: ${row.original.id}`}
            value={row.original.name}
            onChange={(event) => {
              const name = event.currentTarget.value;
              setData((previous) =>
                previous.map((member) =>
                  member.id === row.original.id ? { ...member, name } : member,
                ),
              );
            }}
          />
        ),
      },
      { accessorKey: 'email', header: 'Email', size: 260 },
      { accessorKey: 'team', header: 'Team', size: 140 },
    ],
    [true],
  );
  const table = useReactTable({
    data,
    columns,
    getRowId: (member) => member.id,
    getCoreRowModel: getCoreRowModel(),
  });
  return (
    <DataTable
      table={table}
      aria-label={'Editable member names'}
      pagination={false}
    />
  );
}
```

Expand code

Render Input, Select or other controls through `cell`. Keep data, submission and validation in the application; there is no second form store. Opened detail content stays mounted when collapsed, preserving input state; unopened detail content is not mounted.

## Row and column reordering


Use the handles to reorder rows or columns.

Team priorities| Reorder rows | Member | Team | Status |
| --- | --- | --- | --- |
|  | A**Alex Morgan** | Design | Active |
|  | S**Sam Taylor** | Engineering | Active |
|  | J**Jordan Lee** | Product | Invited |

reordering.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/table/reordering.tsx"
import { Avatar, Space, Tag } from '@ifhover/leaf-ui';

import { DataTable } from '@ifhover/leaf-ui/table';

import type { ColumnDef } from '@tanstack/react-table';

import { useMemo, useState } from 'react';

const members = [
  { id: 'alex', name: 'Alex Morgan', team: 'Design', status: 'active' },
  { id: 'sam', name: 'Sam Taylor', team: 'Engineering', status: 'active' },
  { id: 'jordan', name: 'Jordan Lee', team: 'Product', status: 'invited' },
];

type Member = (typeof members)[number];

export function TableReordering() {
  const [data, setData] = useState(members);
  const columns = useMemo<ColumnDef<Member>[]>(
    () => [
      {
        accessorKey: 'name',
        header: 'Member',
        size: 220,
        cell: ({ row }) => (
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <Avatar size="sm">{row.original.name[0]}</Avatar>
            <strong>{row.original.name}</strong>
          </div>
        ),
      },
      { accessorKey: 'team', header: 'Team' },
      {
        accessorKey: 'status',
        header: 'Status',
        cell: ({ getValue }) => (
          <Tag color={getValue() === 'active' ? 'success' : 'default'}>
            {getValue() === 'active'
              ? 'Active'
              : 'Invited'}
          </Tag>
        ),
      },
    ],
    [true],
  );
  return (
    <Space direction="vertical" align="stretch">
      <p>
        {'Use the handles to reorder rows or columns.'}
      </p>
      <DataTable
        data={data}
        columns={columns}
        rowKey="id"
        caption={'Team priorities'}
        aria-label={'Reorderable team'}
        getRowLabel={(row) => row.original.name}
        options={{ enableSorting: false }}
        pagination={false}
        columnReordering
        onRowReorder={({ rows }) => setData(rows.map((row) => row.original))}
      />
    </Space>
  );
}
```

Expand code

Handles support pointer, touch and keyboard interaction. Focus a handle, press Space to start, use arrow keys to move, Space to drop or Escape to cancel. `onRowReorder` returns the reordered current-page rows and stable identifiers; the application updates its data or submits the server change. Column dragging writes directly to TanStack's `columnOrder`.

Row handles are only shown for flat, nonvirtual tables without active sorting, filtering, grouping or expansion, so display order cannot conflict with business order. Column handles support single-level headers and stay within the same pinned region. For grouped headers, set `columnOrder` explicitly. Reordering preserves row selection and mounted editor state.

## Large data


| Member | Email | Team | Projects | Status |
| --- | --- | --- | --- | --- |
| M**Member 00001** | member1@example.com | Design | 1 | Active |
| M**Member 00002** | member2@example.com | Engineering | 8 | Active |
| M**Member 00003** | member3@example.com | Product | 15 | Invited |
| M**Member 00004** | member4@example.com | Design | 4 | Active |
| M**Member 00005** | member5@example.com | Engineering | 11 | Active |
| M**Member 00006** | member6@example.com | Product | 18 | Active |
| M**Member 00007** | member7@example.com | Design | 7 | Invited |
| M**Member 00008** | member8@example.com | Engineering | 14 | Active |
| M**Member 00009** | member9@example.com | Product | 3 | Active |
| M**Member 00010** | member10@example.com | Design | 10 | Active |
| M**Member 00011** | member11@example.com | Engineering | 17 | Invited |
| M**Member 00012** | member12@example.com | Product | 6 | Active |
|  |

advanced.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/table/advanced.tsx"
import { DataTable } from '@ifhover/leaf-ui/table';

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

import { type Member, members, useMemberColumns } from './data';

const manyMembers: Member[] = Array.from({ length: 10000 }, (_, index) => ({
  ...(members[index % members.length] as Member),
  id: String(index),
  name: `Member ${String(index + 1).padStart(5, '0')}`,
}));

export function TableVirtual() {
  const columns = useMemberColumns(true);
  const table = useReactTable({
    data: manyMembers,
    columns,
    getRowId: (member) => member.id,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
  });
  return (
    <DataTable
      table={table}
      aria-label={'10000 members'}
      virtual={{ estimateRowHeight: 55, overscan: 6 }}
      stickyHeader
      maxHeight={320}
      pagination={false}
    />
  );
}
```

Expand code

Row virtualization is explicit and off by default. Set maxHeight and an appropriate estimated height; dynamic heights are measured and a focused row is retained when scrolling. SSR renders an estimated window before client measurement.

This is not a spreadsheet DataGrid: it does not add arrow-key cell navigation, column virtualization or automatic mobile cards. Tables retain native semantics and horizontal scrolling. For screen-reader users who need all data in the accessibility tree, disable virtualization and use pagination.

## Server data


RefreshSimulate error

| Member | Email | Team | Projects | Status |
| --- | --- | --- | --- | --- |
| Loading |

123456

advanced.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/table/advanced.tsx"
import { Button } from '@ifhover/leaf-ui';

import { DataTable } from '@ifhover/leaf-ui/table';

import { getCoreRowModel, type PaginationState, useReactTable } from '@tanstack/react-table';

import { useEffect, useState } from 'react';

import { type Member, members, useMemberColumns } from './data';

export function TableServer() {
  const columns = useMemberColumns(true);
  const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 5 });
  const [data, setData] = useState<Member[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [request, setRequest] = useState(0);
  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    setError(false);
    const timer = setTimeout(
      () => {
        if (controller.signal.aborted) return;
        setData(
          members.slice(
            pagination.pageIndex * pagination.pageSize,
            (pagination.pageIndex + 1) * pagination.pageSize,
          ),
        );
        setLoading(false);
      },
      request > 0 ? 350 : 500,
    );
    return () => {
      controller.abort();
      clearTimeout(timer);
    };
  }, [pagination, request]);
  const table = useReactTable({
    data,
    columns,
    state: { pagination },
    onPaginationChange: setPagination,
    manualPagination: true,
    manualSorting: true,
    enableSorting: false,
    rowCount: members.length,
    getRowId: (member) => member.id,
    getCoreRowModel: getCoreRowModel(),
  });
  return (
    <DataTable
      table={table}
      aria-label={'Remote members'}
      loading={loading}
      error={error ? ('Unable to load members') : undefined}
      onRetry={() => setRequest((value) => value + 1)}
    >
      <DataTable.Toolbar>
        <Button
          variant="outline"
          disabled={loading}
          onClick={() => setRequest((value) => value + 1)}
        >
          {'Refresh'}
        </Button>
        <Button variant="soft" disabled={loading} onClick={() => setError(true)}>
          {'Simulate error'}
        </Button>
      </DataTable.Toolbar>
      <DataTable.Content />
      <DataTable.Footer>
        <DataTable.Pagination pageSizeOptions={[5, 10, 20]} />
      </DataTable.Footer>
    </DataTable>
  );
}
```

Expand code

Use `manualPagination` with `rowCount`, and `manualSorting` / `manualFiltering` for corresponding server features. Use controlled TanStack state as request parameters and cancel stale requests. Set `pageCount: -1` for unknown totals; the footer then offers only Previous/Next. Supply `<DataTable.Pagination hasNextPage={hasMore} />` to prevent advancing past the available records.

Refreshing keeps existing rows, adds a loading overlay and prevents duplicate interactions. First load, empty data and errors have separate state rows. DataTable automatically renders content and pagination unless pagination is false. Providing children replaces the default layout; explicitly arrange Toolbar, Content and Footer in that case.

## Empty, loading and error


EmptyLoadingError

| Member | Team |
| --- | --- |
| No data |

basic.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/table/basic.tsx"
import { Button, Space } from '@ifhover/leaf-ui';

import { DataTable } from '@ifhover/leaf-ui/table';

import { useState } from 'react';

const people = [
  { id: '1', name: 'Alex Morgan', email: 'alex@example.com', role: 'Design', active: true },
  { id: '2', name: 'Sam Taylor', email: 'sam@example.com', role: 'Engineering', active: true },
  { id: '3', name: 'Jordan Lee', email: 'jordan@example.com', role: 'Product', active: false },
];

const emptyPeople: typeof people = [];

export function TableStates() {
  const [state, setState] = useState<'empty' | 'loading' | 'error'>('empty');
  return (
    <Space direction="vertical" align="stretch">
      <Space wrap>
        <Button variant={state === 'empty' ? 'outline' : 'soft'} onClick={() => setState('empty')}>
          {'Empty'}
        </Button>
        <Button
          variant={state === 'loading' ? 'outline' : 'soft'}
          onClick={() => setState('loading')}
        >
          {'Loading'}
        </Button>
        <Button variant={state === 'error' ? 'outline' : 'soft'} onClick={() => setState('error')}>
          {'Error'}
        </Button>
      </Space>
      <DataTable
        data={emptyPeople}
        columns={[
          { accessorKey: 'name', header: 'Member' },
          { accessorKey: 'role', header: 'Team' },
        ]}
        rowKey="id"
        aria-label={'Data states'}
        pagination={false}
        loading={state === 'loading'}
        error={state === 'error' ? ('Unable to load members') : null}
        onRetry={() => setState('empty')}
      />
    </Space>
  );
}
```

Expand code

## DataTable API

This table describes the instance mode. For the data and columns mode, see [DataTableData API](#datatabledata-api).

| Property            | Type                                                                             | Default  | Description                                                                                  |
| ------------------- | -------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------- |
| `children`          | `ReactNode`                                                                      | —        | Replace the default Content/Pagination layout.                                               |
| `caption`           | `ReactNode`                                                                      | —        | Table caption.                                                                               |
| `size`              | `'sm' \| 'md' \| 'lg'`                                                           | `'md'`   | Presentation size.                                                                           |
| `bordered`          | `boolean`                                                                        | `false`  | Show the outer frame and column dividers. Borderless headers have a 1px bottom divider.      |
| `striped`           | `boolean`                                                                        | `true`   | Alternate row backgrounds and hide row dividers. Disable to show fine dividers between rows. |
| `stickyHeader`      | `boolean`                                                                        | `true`   | Keep headers at the top of the scroll region.                                                |
| `maxHeight`         | `CSSProperties['maxHeight']`                                                     | —        | Maximum scroll height; virtual mode defaults to 400 when omitted.                            |
| `minWidth`          | `CSSProperties['minWidth']`                                                      | —        | Minimum table width; defaults to visible column sizes.                                       |
| `loading`           | `boolean`                                                                        | `false`  | Show a loading state.                                                                        |
| `emptyContent`      | `ReactNode`                                                                      | —        | Empty content.                                                                               |
| `error`             | `ReactNode`                                                                      | —        | Error content; null, undefined and false do not show an error.                               |
| `onRetry`           | `() => void`                                                                     | —        | Retry action.                                                                                |
| `selection`         | `false \| 'single' \| 'multiple'`                                                | `false`  | None, single or multiple row selection.                                                      |
| `selectAllMode`     | `'page' \| 'all'`                                                                | `'page'` | Select the page or all loaded selectable rows.                                               |
| `resizable`         | `boolean`                                                                        | `false`  | Show resize handles for eligible columns.                                                    |
| `columnReordering`  | `boolean`                                                                        | `false`  | Show drag handles for single-level headers.                                                  |
| `virtual`           | `boolean \| `[`DataTableVirtualOptions`](#datatablevirtualoptions)               | `false`  | Explicit row virtualization.                                                                 |
| `pagination`        | `boolean`                                                                        | `true`   | Pagination in the default layout.                                                            |
| `tableRef`          | `Ref<HTMLTableElement>`                                                          | —        | Native table DOM ref.                                                                        |
| `table`             | `TanStackTable<TData>`                                                           | —        | Required. Instance created by useReactTable; the sole data and state owner.                  |
| `getRowLabel`       | `(row: Row<TData>) => string`                                                    | —        | Accessible row-action name; defaults to row\.id.                                             |
| `renderExpandedRow` | `(row: Row<TData>) => ReactNode`                                                 | —        | Leaf-row details; configure getRowCanExpand.                                                 |
| `getRowProps`       | `(row: Row<TData>) => `[`DataTableRowProps`](#datatablerow-api)                  | —        | Custom row attributes/events.                                                                |
| `getCellProps`      | `(cell: Cell<TData, unknown>) => `[`DataTableCellProps`](#datatablecell-api)     | —        | Cell attributes, styles or spans; a zero colSpan / rowSpan omits the cell.                   |
| `onRowReorder`      | `(info: `[`DataTableRowReorderInfo`](#datatablerowreorderinfo)`<TData>) => void` | —        | Show row handles and return reordered current-page rows; the application updates the data.   |

aria-label/aria-labelledby name the native table; other div attributes apply to the wrapper.

## DataTableRow API

`DataTableRowProps` configures native `tr` attributes and events through `getRowProps`.

| Property   | Type      | Default | Description                        |
| ---------- | --------- | ------- | ---------------------------------- |
| `selected` | `boolean` | —       | Apply the selected row appearance. |

## DataTableCell API

`DataTableCellProps` configures native `td` attributes, spans and events through `getCellProps`.

| Property    | Type                           | Default   | Description                       |
| ----------- | ------------------------------ | --------- | --------------------------------- |
| `align`     | `'start' \| 'center' \| 'end'` | `'start'` | Cell content alignment.           |
| `pinned`    | `'start' \| 'end'`             | —         | Pin to the logical start or end.  |
| `pinOffset` | `number`                       | `0`       | Pixel offset from that side.      |
| `pinEdge`   | `boolean`                      | —         | Show a subtle pinned-edge shadow. |

## DataTableSearch API

| Property       | Type                                                            | Default | Description                                 |
| -------------- | --------------------------------------------------------------- | ------- | ------------------------------------------- |
| `size`         | `'sm' \| 'md' \| 'lg'`                                          | —       | Inherit DataTable size by default.          |
| `status`       | `'error' \| 'warning'`                                          | —       | Input validation status.                    |
| `prefix`       | `ReactNode`                                                     | —       | Leading content; defaults to a search icon. |
| `suffix`       | `ReactNode`                                                     | —       | Trailing content.                           |
| `allowClear`   | `boolean`                                                       | `true`  | Show a clear action.                        |
| `onClear`      | `() => void`                                                    | —       | Called when the search is cleared.          |
| `showCount`    | `boolean \| ((value: string, maxLength?: number) => ReactNode)` | —       | Display or customize the input count.       |
| `onPressEnter` | `KeyboardEventHandler<HTMLInputElement>`                        | —       | Called on Enter.                            |
| `columnId`     | `string`                                                        | —       | Filter one column; omit for global search.  |

Other Input attributes are supported; value and onChange belong to the instance.

## DataTableColumnVisibility API

| Property   | Type        | Default | Description            |
| ---------- | ----------- | ------- | ---------------------- |
| `children` | `ReactNode` | —       | Custom button text.    |
| `label`    | `string`    | —       | Accessible panel name. |

## DataTablePagination API

| Property           | Type                                                                          | Default     | Description                                                                             |
| ------------------ | ----------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------- |
| `showSizeChanger`  | `boolean`                                                                     | `true`      | Show the page-size selector.                                                            |
| `pageSizeOptions`  | `readonly number[]`                                                           | —           | Available page sizes.                                                                   |
| `showQuickJumper`  | `boolean`                                                                     | `false`     | Commit a quick jump on Enter or when leaving the pagination region.                     |
| `showTotal`        | `boolean \| ((total: number, range: readonly [number, number]) => ReactNode)` | `false`     | true shows a localized total; pass a function to customize the total and visible range. |
| `simple`           | `boolean`                                                                     | —           | Use compact pagination.                                                                 |
| `disabled`         | `boolean`                                                                     | —           | Disable interaction.                                                                    |
| `hideOnSinglePage` | `boolean`                                                                     | —           | Hide when only one page exists.                                                         |
| `size`             | `'sm' \| 'md' \| 'lg'`                                                        | —           | Inherit DataTable size by default.                                                      |
| `variant`          | `'outline' \| 'ghost'`                                                        | `'outline'` | Pagination button appearance: outlined or borderless.                                   |
| `hasNextPage`      | `boolean`                                                                     | —           | Control next-page availability when the total is unknown.                               |

Supports Pagination display options. Total, current, pageSize and onChange belong to the instance; showSizeChanger defaults to true. Unknown totals use Previous/Next only.

## DataTableContent API

| Property    | Type            | Default | Description                           |
| ----------- | --------------- | ------- | ------------------------------------- |
| `className` | `string`        | —       | Custom class for the content region.  |
| `style`     | `CSSProperties` | —       | Inline styles for the content region. |

`DataTable.Content` accepts native `div` attributes, including `className`, `style` and events. Configure table layout, selection, expansion and loading on [DataTable](#datatable-api); use `tableRef` for the table DOM ref. The exported type is `DataTableContentProps`.

## DataTableColumnMeta

Use this exported type to assemble the following optional properties on TanStack column meta. Supply label for functional headers.

| Property          | Type                           | Default   | Description                              |
| ----------------- | ------------------------------ | --------- | ---------------------------------------- |
| `label`           | `string`                       | —         | Plain-text column name used by controls. |
| `align`           | `'start' \| 'center' \| 'end'` | `'start'` | Header and cell alignment.               |
| `cellClassName`   | `string`                       | —         | Cell class.                              |
| `headerClassName` | `string`                       | —         | Header class.                            |

## DataTableVirtualOptions

| Property            | Type     | Default | Description                                |
| ------------------- | -------- | ------- | ------------------------------------------ |
| `estimateRowHeight` | `number` | `48`    | Estimated height before measuring.         |
| `overscan`          | `number` | `5`     | Additional rows before/after the viewport. |

## Types and composition

```ts
import type { DataTableProps, DataTableDataProps, DataTableRowProps, DataTableCellProps, DataTableSearchProps, DataTableColumnVisibilityProps, DataTablePaginationProps, DataTableContentProps, DataTableColumnMeta, DataTableVirtualOptions, DataTableRowReorderInfo } from '@ifhover/leaf-ui/table';
import type { ColumnDef, Table as TanStackTable, Row, Cell } from '@tanstack/react-table';
```

DataTable.Toolbar/Footer/Content accept div attributes. Use TanStack's own [column definitions](https://tanstack.com/table/v8/docs/guide/column-defs), [controlled state](https://tanstack.com/table/v8/docs/framework/react/guide/table-state), [row selection](https://tanstack.com/table/v8/docs/guide/row-selection) and [pinning](https://tanstack.com/table/v8/docs/guide/column-pinning) rather than a second Leaf column model.

## DataTableData API

Additional convenience-mode properties are listed below. Presentation, selection, expansion and scrolling options match [DataTable API](#datatable-api); `table` is not accepted. The exported type is `DataTableDataProps<TData>`.

| Property  | Type                                                                                | Default | Description                                                                      |
| --------- | ----------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------------------- |
| `data`    | `readonly TData[]`                                                                  | —       | Required. Data to display; provide a new array when updating.                    |
| `columns` | `ColumnDef<TData>[]`                                                                | —       | Required. Native TanStack column definitions.                                    |
| `rowKey`  | `keyof TData \| ((row: TData, index: number) => string)`                            | —       | Required. A stable unique key field or a function returning a string identifier. |
| `options` | `Omit<TableOptions<TData>, 'data' \| 'columns' \| 'getCoreRowModel' \| 'getRowId'>` | —       | Customize TanStack state, callbacks and server configuration.                    |

## DataTableRowReorderInfo

| Property    | Type                    | Default | Description                                                                   |
| ----------- | ----------------------- | ------- | ----------------------------------------------------------------------------- |
| `rows`      | `readonly Row<TData>[]` | —       | Required. Reordered current-page rows; read business data from row\.original. |
| `fromIndex` | `number`                | —       | Required. Original zero-based current-page index.                             |
| `toIndex`   | `number`                | —       | Required. Destination zero-based current-page index.                          |
| `activeRow` | `Row<TData>`            | —       | Required. The moved row, including its stable row\.id.                        |
| `overRow`   | `Row<TData>`            | —       | Required. The target row, including its stable row\.id.                       |

## DataTablePresentation API

ConfigProvider's `components.DataTable` uses these row-independent props. Import `DataTablePresentationProps` from `@ifhover/leaf-ui` to type reusable defaults. Configuring defaults alone does not require TanStack Table; using DataTable still requires it.

| Property           | Type                                                               | Default | Description                                       |
| ------------------ | ------------------------------------------------------------------ | ------- | ------------------------------------------------- |
| `children`         | `ReactNode`                                                        | —       | Content, or child nodes for hierarchical records. |
| `caption`          | `ReactNode`                                                        | —       | Configure caption.                                |
| `size`             | `'sm' \| 'md' \| 'lg'`                                             | —       | Presentation size.                                |
| `bordered`         | `boolean`                                                          | —       | Configure bordered.                               |
| `striped`          | `boolean`                                                          | —       | Configure striped.                                |
| `stickyHeader`     | `boolean`                                                          | —       | Configure sticky header.                          |
| `maxHeight`        | `CSSProperties['maxHeight']`                                       | —       | Configure max height.                             |
| `minWidth`         | `CSSProperties['minWidth']`                                        | —       | Configure min width.                              |
| `loading`          | `boolean`                                                          | —       | Show a loading state.                             |
| `emptyContent`     | `ReactNode`                                                        | —       | Content for an empty collection.                  |
| `error`            | `ReactNode`                                                        | —       | Configure error.                                  |
| `onRetry`          | `() => void`                                                       | —       | Configure on retry.                               |
| `selection`        | `false \| 'single' \| 'multiple'`                                  | —       | Configure selection.                              |
| `selectAllMode`    | `'page' \| 'all'`                                                  | —       | Configure select all mode.                        |
| `resizable`        | `boolean`                                                          | —       | Allow resizing by dragging or arrow keys.         |
| `columnReordering` | `boolean`                                                          | —       | Configure column reordering.                      |
| `virtual`          | `boolean \| `[`DataTableVirtualOptions`](#datatablevirtualoptions) | —       | Enable row virtualization; disabled by default.   |
| `pagination`       | `boolean`                                                          | —       | Configure pagination.                             |
| `tableRef`         | `Ref<HTMLTableElement>`                                            | —       | Configure table ref.                              |
