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, and this page is available as Markdown at https://ifhover.github.io/leaf-ui/v/0.6.0/en/components/table.md.
  • English
  • 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.
    import { Table } from '@ifhover/leaf-ui/table';

    For DataTable, install the optional peer dependency:

    npm install @tanstack/react-table@^8.21.3
    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. DataTable and its types are available only from the adapter subpath, not the library root.

    Basic usage

    MemberEmailTeamStatus
    Alex Morganalex@example.comDesignActive
    Sam Taylorsam@example.comEngineeringActive
    Jordan Leejordan@example.comProductInvited
    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>
      );
    }
    

    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

    Alex Morganalex@example.comDesign
    Sam Taylorsam@example.comEngineering
    Jordan Leejordan@example.comProduct
    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}
        />
      );
    }
    

    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 selected
    Expand rows
    Alex Morgan
    member1@example.comDesign1Active
    Sam Taylor
    member2@example.comEngineering8Active
    Jordan Lee
    member3@example.comProduct15Invited
    Riley Chen
    member4@example.comDesign4Active
    Casey Wilson
    member5@example.comEngineering11Active
    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>
      );
    }
    

    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:

    FeatureTanStack configuration
    DatagetCoreRowModel()
    Client sortinggetSortedRowModel()
    Client filteringgetFilteredRowModel()
    Client paginationgetPaginationRowModel()
    Tree/group expansiongetExpandedRowModel() and getSubRows for trees
    Detail rowsgetRowCanExpand and DataTable renderExpandedRow
    Grouping and totalsgetGroupedRowModel(), 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.





    Alex Morgan
    member1@example.comDesign1Active
    Sam Taylor
    member2@example.comEngineering8Active
    Jordan Lee
    member3@example.comProduct15Invited
    Riley Chen
    member4@example.comDesign4Active
    Casey Wilson
    member5@example.comEngineering11Active
    Jamie Park
    member6@example.comProduct18Active
    Alex Morgan
    member7@example.comDesign7Invited
    Sam Taylor
    member8@example.comEngineering14Active
    Jordan Lee
    member9@example.comProduct3Active
    Riley Chen
    member10@example.comDesign10Active
    Casey Wilson
    member11@example.comEngineering17Invited
    Jamie Park
    member12@example.comProduct6Active
    Alex Morgan
    member13@example.comDesign13Active
    Sam Taylor
    member14@example.comEngineering2Active
    Jordan Lee
    member15@example.comProduct9Invited
    Riley Chen
    member16@example.comDesign16Active
    Casey Wilson
    member17@example.comEngineering5Active
    Jamie Park
    member18@example.comProduct12Active
    Alex Morgan
    member19@example.comDesign1Invited
    Sam Taylor
    member20@example.comEngineering8Active
    Jordan Lee
    member21@example.comProduct15Active
    Riley Chen
    member22@example.comDesign4Active
    Casey Wilson
    member23@example.comEngineering11Invited
    Jamie Park
    member24@example.comProduct18Active
    Alex Morgan
    member25@example.comDesign7Active
    Sam Taylor
    member26@example.comEngineering14Active
    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>
      );
    }
    

    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; 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
    Design (3)Total: 12
    Alex Morgan1
    Riley Chen4
    Alex Morgan7
    Engineering (3)Total: 33
    Sam Taylor8
    Casey Wilson11
    Sam Taylor14
    Product (2)Total: 33
    Jordan Lee15
    Jamie Park18
    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}
        />
      );
    }
    

    Editable cells

    member1@example.comDesign
    member2@example.comEngineering
    member3@example.comProduct
    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}
        />
      );
    }
    

    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
    Alex Morgan
    DesignActive
    Sam Taylor
    EngineeringActive
    Jordan Lee
    ProductInvited
    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>
      );
    }
    

    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 00001
    member1@example.comDesign1Active
    Member 00002
    member2@example.comEngineering8Active
    Member 00003
    member3@example.comProduct15Invited
    Member 00004
    member4@example.comDesign4Active
    Member 00005
    member5@example.comEngineering11Active
    Member 00006
    member6@example.comProduct18Active
    Member 00007
    member7@example.comDesign7Invited
    Member 00008
    member8@example.comEngineering14Active
    Member 00009
    member9@example.comProduct3Active
    Member 00010
    member10@example.comDesign10Active
    Member 00011
    member11@example.comEngineering17Invited
    Member 00012
    member12@example.comProduct6Active
    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}
        />
      );
    }
    

    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

    Member
    Email
    Team
    Projects
    Status
    Loading
    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>
      );
    }
    

    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

    MemberTeam
    No data
    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>
      );
    }
    

    Table API

    PropertyTypeDefaultDescription
    size'sm' | 'md' | 'lg''md'Row size.
    borderedbooleanfalseShow column dividers.
    stripedbooleanfalseAlternate row backgrounds.
    stickyHeaderbooleantrueKeep the header at the top of the scroll area.
    maxHeightCSSProperties['maxHeight']—Scroll-area height limit.
    minWidthCSSProperties['minWidth']—Minimum table width.
    tableLayoutCSSProperties['tableLayout']—Native layout mode.
    tableStyleCSSProperties—Styles on table; style and className apply to the wrapper.
    scrollRefRef<HTMLDivElement>—Scroll-area ref; the component ref points to table.
    loadingbooleanfalseKeep content and show a loading overlay.

    TableRow API

    PropertyTypeDefaultDescription
    selectedboolean—Selected background; selection controls convey interaction state.

    TableCell API

    PropertyTypeDefaultDescription
    align'start' | 'center' | 'end''start'Content alignment.
    pinned'start' | 'end'—Pin to the logical start or end side.
    pinOffsetnumber0Pixel offset from that side.
    pinEdgeboolean—Show a subtle pinned-edge shadow.

    TableColumnHeader API

    PropertyTypeDefaultDescription
    align'start' | 'center' | 'end''start'Header content alignment.
    pinned'start' | 'end'—Pin to the logical start or end.
    pinOffsetnumber0Pixel offset from that side.
    pinEdgeboolean—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.

    PropertyTypeDefaultDescription
    colSpannumber1Number of columns covered.
    childrenReactNode—Custom state content.

    TableError API

    PropertyTypeDefaultDescription
    colSpannumber1Number of columns covered.
    childrenReactNode—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.

    PropertyTypeDefaultDescription
    childrenReactNode—Replace the default Content/Pagination layout.
    captionReactNode—Table caption.
    size'sm' | 'md' | 'lg''md'Presentation size.
    borderedbooleanfalseShow vertical column separators.
    stripedbooleanfalseAlternate row backgrounds.
    stickyHeaderbooleantrueKeep headers at the top of the scroll region.
    maxHeightTableProps['maxHeight']—Maximum scroll height; virtual mode defaults to 400 when omitted.
    minWidthTableProps['minWidth']—Minimum table width; defaults to visible column sizes.
    loadingbooleanfalseShow a loading state.
    emptyContentReactNode—Empty content.
    errorReactNode—Error content; null, undefined and false do not show an error.
    onRetry() => void—Retry action.
    selectionfalse | 'single' | 'multiple'falseNone, single or multiple row selection.
    selectAllMode'page' | 'all''page'Select the page or all loaded selectable rows.
    resizablebooleanfalseShow resize handles for eligible columns.
    columnReorderingbooleanfalseShow drag handles for single-level headers.
    virtualboolean | DataTableVirtualOptionsfalseExplicit row virtualization.
    paginationbooleantruePagination in the default layout.
    tableRefRef<HTMLTableElement>—Native table DOM ref.
    tableTanStackTable<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—Custom row attributes/events.
    getCellProps(cell: Cell<TData, unknown>) => TableCellProps—Cell attributes, styles or spans; a zero colSpan / rowSpan omits the cell.
    onRowReorder(info: 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

    PropertyTypeDefaultDescription
    size'sm' | 'md' | 'lg'—Inherit DataTable size by default.
    status'error' | 'warning'—Input validation status.
    prefixReactNode—Leading content; defaults to a search icon.
    suffixReactNode—Trailing content.
    allowClearbooleantrueShow a clear action.
    onClear() => void—Called when the search is cleared.
    showCountboolean | ((value: string, maxLength?: number) => ReactNode)—Display or customize the input count.
    onPressEnterKeyboardEventHandler<HTMLInputElement>—Called on Enter.
    columnIdstring—Filter one column; omit for global search.

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

    DataTableColumnVisibility API

    PropertyTypeDefaultDescription
    childrenReactNode—Custom button text.
    labelstring—Accessible panel name.

    DataTablePagination API

    PropertyTypeDefaultDescription
    showSizeChangerbooleantrueShow the page-size selector.
    pageSizeOptionsreadonly number[]—Available page sizes.
    showQuickJumperbooleanfalseCommit a quick jump on Enter or when leaving the pagination region.
    showTotalboolean | ((total: number, range: readonly [number, number]) => ReactNode)falsetrue shows a localized total; pass a function to customize the total and visible range.
    simpleboolean—Use compact pagination.
    disabledboolean—Disable interaction.
    hideOnSinglePageboolean—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.
    hasNextPageboolean—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

    PropertyTypeDefaultDescription
    classNamestring—Custom class for the content region.
    styleCSSProperties—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; 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.

    PropertyTypeDefaultDescription
    labelstring—Plain-text column name used by controls.
    align'start' | 'center' | 'end''start'Header and cell alignment.
    cellClassNamestring—Cell class.
    headerClassNamestring—Header class.

    DataTableVirtualOptions

    PropertyTypeDefaultDescription
    estimateRowHeightnumber48Estimated height before measuring.
    overscannumber5Additional rows before/after the viewport.

    Types and composition

    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, controlled state, row selection and 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; table is not accepted. The exported type is DataTableDataProps<TData>.

    PropertyTypeDefaultDescription
    datareadonly TData[]—Required. Data to display; provide a new array when updating.
    columnsColumnDef<TData>[]—Required. Native TanStack column definitions.
    rowKeykeyof TData | ((row: TData, index: number) => string)—Required. A stable unique key field or a function returning a string identifier.
    optionsOmit<TableOptions<TData>, 'data' | 'columns' | 'getCoreRowModel' | 'getRowId'>—Customize TanStack state, callbacks and server configuration.

    DataTableRowReorderInfo

    PropertyTypeDefaultDescription
    rowsreadonly Row<TData>[]—Required. Reordered current-page rows; read business data from row.original.
    fromIndexnumber—Required. Original zero-based current-page index.
    toIndexnumber—Required. Destination zero-based current-page index.
    activeRowRow<TData>—Required. The moved row, including its stable row.id.
    overRowRow<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.

    PropertyTypeDefaultDescription
    childrenReactNode—Content, or child nodes for hierarchical records.
    captionReactNode—Configure caption.
    size'sm' | 'md' | 'lg'—Presentation size.
    borderedboolean—Configure bordered.
    stripedboolean—Configure striped.
    stickyHeaderboolean—Configure sticky header.
    maxHeightTableProps['maxHeight']—Configure max height.
    minWidthTableProps['minWidth']—Configure min width.
    loadingboolean—Show a loading state.
    emptyContentReactNode—Content for an empty collection.
    errorReactNode—Configure error.
    onRetry() => void—Configure on retry.
    selectionfalse | 'single' | 'multiple'—Configure selection.
    selectAllMode'page' | 'all'—Configure select all mode.
    resizableboolean—Allow resizing by dragging or arrow keys.
    columnReorderingboolean—Configure column reordering.
    virtualboolean | DataTableVirtualOptions—Enable virtual scrolling; enabled by default for large option sets.
    paginationboolean—Configure pagination.
    tableRefRef<HTMLTableElement>—Configure table ref.