> Leaf UI 0.6.0 · Package: @ifhover/leaf-ui · Source: b30c3d3a8fa9c5e15d0d0522cebfbd33f60c6b06
> Browser documentation: https://ifhover.github.io/leaf-ui/v/0.6.0/en/components/table.html

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

# Table

Browse, filter and manage structured data.

## Choose an API

- **Table** composes native headers, rows and cells. Use it for static or fully custom tables without installing a table engine.
- **DataTable** accepts data and columns for quick sorting and pagination, or your own TanStack Table instance for filtering, selection, expansion and column controls.

```tsx
import { Table } from '@ifhover/leaf-ui/table';
```

For DataTable, install the optional peer dependency:

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

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

Component subpaths load their styles automatically. Import the base stylesheet once as shown in [getting started](https://ifhover.github.io/leaf-ui/v/0.6.0/en/guide/getting-started.md). DataTable and its types are available only from the adapter subpath, not 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 { Table, Tag } from '@ifhover/leaf-ui';

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 (
    <Table aria-label={'Team members'} minWidth={560}>
      <Table.Header>
        <Table.Row>
          <Table.ColumnHeader>{'Member'}</Table.ColumnHeader>
          <Table.ColumnHeader>{'Email'}</Table.ColumnHeader>
          <Table.ColumnHeader>{'Team'}</Table.ColumnHeader>
          <Table.ColumnHeader>{'Status'}</Table.ColumnHeader>
        </Table.Row>
      </Table.Header>
      <Table.Body>
        {people.map((person) => (
          <Table.Row key={person.id}>
            <Table.Cell>
              <strong>{person.name}</strong>
            </Table.Cell>
            <Table.Cell>{person.email}</Table.Cell>
            <Table.Cell>{person.role}</Table.Cell>
            <Table.Cell>
              <Tag color={person.active ? 'success' : 'default'}>
                {person.active ? ('Active') : 'Invited'}
              </Tag>
            </Table.Cell>
          </Table.Row>
        ))}
      </Table.Body>
    </Table>
  );
}
```

Expand code

`Table` and `Table.Root` are equivalent. Native attributes, DOM refs, `colSpan`, `rowSpan` and custom cell content are supported. Name the table with Table.Caption, aria-label or aria-labelledby.

## Quick data table


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

basic.tsx

Copy code

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

const simpleColumns = [
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'email', header: 'Email' },
  { accessorKey: 'role', header: 'Team' },
];

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 TableSimple() {
  return (
    <DataTable
      data={people}
      columns={simpleColumns}
      rowKey="id"
      aria-label={'Simple data table'}
      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.

## 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/tanstack';

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/tanstack';

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="ghost"
            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/v/0.6.0/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/tanstack';

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

## 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/tanstack';

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/tanstack';

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/tanstack';

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/tanstack';

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="ghost" 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, Table } from '@ifhover/leaf-ui';

import { useState } from 'react';

export function TableStates() {
  const [state, setState] = useState<'empty' | 'loading' | 'error'>('empty');
  return (
    <Space direction="vertical" align="stretch">
      <Space wrap>
        <Button variant={state === 'empty' ? 'outline' : 'ghost'} onClick={() => setState('empty')}>
          {'Empty'}
        </Button>
        <Button
          variant={state === 'loading' ? 'outline' : 'ghost'}
          onClick={() => setState('loading')}
        >
          {'Loading'}
        </Button>
        <Button variant={state === 'error' ? 'outline' : 'ghost'} onClick={() => setState('error')}>
          {'Error'}
        </Button>
      </Space>
      <Table aria-label={'Data states'}>
        <Table.Header>
          <Table.Row>
            <Table.ColumnHeader>{'Member'}</Table.ColumnHeader>
            <Table.ColumnHeader>{'Team'}</Table.ColumnHeader>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {state === 'empty' ? (
            <Table.Empty colSpan={2} />
          ) : state === 'loading' ? (
            <Table.Loading colSpan={2} />
          ) : (
            <Table.Error colSpan={2} onRetry={() => setState('empty')} />
          )}
        </Table.Body>
      </Table>
    </Space>
  );
}
```

Expand code

## Table API

| Property       | Type                           | Default | Description                                                |
| -------------- | ------------------------------ | ------- | ---------------------------------------------------------- |
| `size`         | `'sm' \| 'md' \| 'lg'`         | `'md'`  | Row size.                                                  |
| `bordered`     | `boolean`                      | `false` | Show column dividers.                                      |
| `striped`      | `boolean`                      | `false` | Alternate row backgrounds.                                 |
| `stickyHeader` | `boolean`                      | `true`  | Keep the header at the top of the scroll area.             |
| `maxHeight`    | `CSSProperties['maxHeight']`   | —       | Scroll-area height limit.                                  |
| `minWidth`     | `CSSProperties['minWidth']`    | —       | Minimum table width.                                       |
| `tableLayout`  | `CSSProperties['tableLayout']` | —       | Native layout mode.                                        |
| `tableStyle`   | `CSSProperties`                | —       | Styles on table; style and className apply to the wrapper. |
| `scrollRef`    | `Ref<HTMLDivElement>`          | —       | Scroll-area ref; the component ref points to table.        |
| `loading`      | `boolean`                      | `false` | Keep content and show a loading overlay.                   |

## TableRow API

| Property   | Type      | Default | Description                                                       |
| ---------- | --------- | ------- | ----------------------------------------------------------------- |
| `selected` | `boolean` | —       | Selected background; selection controls convey interaction state. |

## TableCell API

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

## TableColumnHeader API

| Property    | Type                           | Default   | Description                       |
| ----------- | ------------------------------ | --------- | --------------------------------- |
| `align`     | `'start' \| 'center' \| 'end'` | `'start'` | Header 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. |

Supports native th attributes and the same alignment/pinning options as TableCell. Scope defaults to col; use colgroup for grouped headers.

## TableState API

For Table.Empty and Table.Loading inside Table.Body.

| Property   | Type        | Default | Description                |
| ---------- | ----------- | ------- | -------------------------- |
| `colSpan`  | `number`    | `1`     | Number of columns covered. |
| `children` | `ReactNode` | —       | Custom state content.      |

## TableError API

| Property   | Type         | Default | Description                        |
| ---------- | ------------ | ------- | ---------------------------------- |
| `colSpan`  | `number`     | `1`     | Number of columns covered.         |
| `children` | `ReactNode`  | —       | Custom error content.              |
| `onRetry`  | `() => void` | —       | Show a retry action when supplied. |

## 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 vertical column separators.                                                           |
| `striped`           | `boolean`                                                                        | `false`  | Alternate row backgrounds.                                                                 |
| `stickyHeader`      | `boolean`                                                                        | `true`   | Keep headers at the top of the scroll region.                                              |
| `maxHeight`         | [`TableProps`](#table-api)`['maxHeight']`                                        | —        | Maximum scroll height; virtual mode defaults to 400 when omitted.                          |
| `minWidth`          | [`TableProps`](#table-api)`['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>) => `[`TableRowProps`](#tablerow-api)                          | —        | Custom row attributes/events.                                                              |
| `getCellProps`      | `(cell: Cell<TData, unknown>) => `[`TableCellProps`](#tablecell-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. |

Size, borders, stripes, sticky headers, scrolling and loading match Table. aria-label/aria-labelledby name the native table; other div attributes apply to the wrapper.

## 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 { TableProps, TableRowProps, TableCellProps, TableColumnHeaderProps, TableStateProps, TableErrorProps } from '@ifhover/leaf-ui/table';
import type { DataTableProps, DataTableDataProps, DataTableSearchProps, DataTableColumnVisibilityProps, DataTablePaginationProps, DataTableContentProps, DataTableColumnMeta, DataTableVirtualOptions, DataTableRowReorderInfo } from '@ifhover/leaf-ui/table/tanstack';
import type { ColumnDef, Table as TanStackTable, Row, Cell } from '@tanstack/react-table';
```

Header/Body/Footer, Caption, Row and Cell forward their native attributes and DOM refs. 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`        | [`TableProps`](#table-api)`['maxHeight']`                          | —       | Configure max height.                                               |
| `minWidth`         | [`TableProps`](#table-api)`['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 virtual scrolling; enabled by default for large option sets. |
| `pagination`       | `boolean`                                                          | —       | Configure pagination.                                               |
| `tableRef`         | `Ref<HTMLTableElement>`                                            | —       | Configure table ref.                                                |
