> For AI agents: the complete documentation index is available at https://wallarm.github.io/design-system/llms.txt, the full documentation bundle is available at https://wallarm.github.io/design-system/llms-full.txt.

# Table

The grid for many records of one shape — reach for a `Card` grid when a row reads better as an object than as a set of columns, and for `Attribute` when you are showing one record rather than many. Everything interactive is controlled: sorting, selection, expansion, column order, pinning, sizing and visibility are each a state prop with a callback, so nothing moves unless you move it, and `manualSorting` hands the sort to the server. Filtering belongs above the grid in a `FilterInput`, and a long collection scrolls rather than paginates — `Pagination` is the deliberate exception, for when a page number is part of the task.

[Source](https://github.com/wallarm/design-system/tree/main/packages/design-system/src/components/Table) · [Storybook](https://wallarm.github.io/design-system/storybook/?path=/docs/data-display-table--overview)

```tsx
import { useState } from 'react';
import { Table, type TableSortingState } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const Basic = () => {
  const [sorting, setSorting] = useState<TableSortingState>([{ id: 'firstDetected', desc: true }]);

  return (
    <Table
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      sorting={sorting}
      onSortingChange={setSorting}
    />
  );
};
```

## Usage

```tsx
import { Table, type TableSortingState } from '@wallarm-org/design-system/Table';
```

## Examples

### No stretch

`stretch={false}` — columns keep their defined widths and do not expand to fill the container.
Empty space appears on the right when the total column width is less than the container width.

```tsx
import { Table, type TableColumnDef } from '@wallarm-org/design-system/Table';
import { type SecurityEvent, securityColumnHelper, securityEvents } from './mocks';

export const NoStretch = () => {
  const narrowColumns: TableColumnDef<SecurityEvent>[] = [
    securityColumnHelper.accessor('objectName', {
      header: 'Object name',
      size: 200,
    }),
    securityColumnHelper.accessor('status', {
      header: 'Status',
      size: 120,
    }),
    securityColumnHelper.accessor('requests', {
      header: 'Requests',
      size: 100,
    }),
  ];

  return (
    <Table data={securityEvents} columns={narrowColumns} getRowId={row => row.id} stretch={false} />
  );
};
```

### With pagination

`useClientPagination` slices the rows and the pager sits right-aligned below the table,
sharing its footer with the rows-per-page control.

```tsx
import { useMemo } from 'react';
import { Pagination, PaginationList, PaginationNext, PaginationPageSize, PaginationPrevious, useClientPagination } from '@wallarm-org/design-system/Pagination';
import { VStack } from '@wallarm-org/design-system/Stack';
import { Table } from '@wallarm-org/design-system/Table';
import { createLargeSecurityEvents, securityColumns } from './mocks';

export const WithPagination = () => {
  const allData = useMemo(() => createLargeSecurityEvents(47), []);
  const { pageData, ...pagination } = useClientPagination(allData, 10);

  return (
    <VStack gap={12}>
      <Table data={pageData} columns={securityColumns} getRowId={row => row.id} />
      <Pagination {...pagination} align='right' aria-label='Security events'>
        <PaginationPageSize options={[10, 25, 50]} />
        <PaginationPrevious />
        <PaginationList />
        <PaginationNext />
      </Pagination>
    </VStack>
  );
};
```

### Sorting

Sorting held in state, so the arrow and the row order are the same fact — click a header to
cycle through the directions.

```tsx
import { useState } from 'react';
import { Table, type TableSortingState } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const Sorting = () => {
  const [sorting, setSorting] = useState<TableSortingState>([]);

  return (
    <Table
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      sorting={sorting}
      onSortingChange={setSorting}
    />
  );
};
```

### Manual sorting

`manualSorting` stops the table sorting anything itself: it only reports the change, and the
story re-derives `data` the way a refetch would.

```tsx
import { useMemo, useState } from 'react';
import { Table, type TableSortingState } from '@wallarm-org/design-system/Table';
import { securityColumns } from './mocks';

export const ManualSorting = () => {
  const [sorting, setSorting] = useState<TableSortingState>([]);
  const data = useMemo(() => sortSecurityEvents(sorting), [sorting]);

  return (
    <Table
      data={data}
      columns={securityColumns}
      getRowId={row => row.id}
      sorting={sorting}
      onSortingChange={setSorting}
      manualSorting
      data-testid='manual-sort-table'
    />
  );
};
```

### With analytics

Every analytics seam at once — `TableSortTrigger`, `TableColumnMenu` and each of its items,
the action bar, the scroll handlers — plus the state callbacks, which carry the resulting
sort, selection and column layout rather than just a click.

```tsx
import { useCallback, useMemo, useState } from 'react';
import { BulkBarSummaryClear, BulkBarSummaryCount, BulkBarSummarySelectAll, BulkBarSummarySeparator } from '@wallarm-org/design-system/BulkBar';
import { Button } from '@wallarm-org/design-system/Button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuItemIcon, DropdownMenuItemText, DropdownMenuSeparator, DropdownMenuTrigger } from '@wallarm-org/design-system/DropdownMenu';
import { Copy, Ellipsis, Filter, FilterX, Trash2 } from '@wallarm-org/design-system/icons';
import { HStack, VStack } from '@wallarm-org/design-system/Stack';
import { Table, TableActionBar, TableActionBarSelection, TableColumnMenu, TableColumnMenuHideItem, TableColumnMenuMoveLeftItem, TableColumnMenuMoveRightItem, TableColumnMenuPinItem, TableColumnMenuSortItem, TableScrollHandler, TableScrollHandlerLeft, TableScrollHandlerRight, TableSortTrigger, type TableColumnDef, type TableColumnPinningState, type TableColumnSizingState, type TableExpandedState, type TableRowSelectionState, type TableSortingState, type TableVisibilityState } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { type SecurityEvent, securityColumnHelper, securityEvents } from './mocks';

export const WithAnalytics = () => {
  const [sorting, setSorting] = useState<TableSortingState>([]);
  const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
  const [expanded, setExpanded] = useState<TableExpandedState>({});
  const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});
  const [columnOrder, setColumnOrder] = useState<string[]>([]);
  const [columnPinning, setColumnPinning] = useState<TableColumnPinningState>({});
  const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});

  // Pretend SDK call — every analytics signal in this story funnels through
  // it so the Storybook actions panel shows one consistent stream.
  const track = useCallback(
    (event: string, props?: Record<string, unknown>) =>
      // biome-ignore lint/suspicious/noConsole: demo story stand-in for an SDK call
      console.info('[analytics]', event, props ?? {}),
    [],
  );

  const columnsWithAnalytics = useMemo<TableColumnDef<SecurityEvent>[]>(
    () => [
      // Master column — sortable header, link cell with analytics, per-row
      // menu action with analytics on every menu item.
      securityColumnHelper.accessor('objectName', {
        header: ({ column }) => (
          <TableSortTrigger
            column={column}
            data-analytics-id='SECURITY_SORT_OBJECT_NAME'
            data-analytics-props='{"surface":"events-table"}'
          >
            Object name
          </TableSortTrigger>
        ),
        size: 320,
        enableSorting: true,
        meta: {
          sortType: 'text' as const,
          resizeType: 'cut' as const,
          renderMenuAction: row => (
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <Button
                  variant='ghost'
                  color='neutral'
                  size='small'
                  aria-label='Row actions'
                  data-analytics-id='SECURITY_ROW_MENU_OPEN'
                  data-analytics-props={`{"rowId":"${row.original.id}"}`}
                >
                  <Ellipsis />
                </Button>
              </DropdownMenuTrigger>
              <DropdownMenuContent>
                <DropdownMenuItem
                  data-analytics-id='SECURITY_ROW_COPY_NAME'
                  data-analytics-props={`{"rowId":"${row.original.id}"}`}
                  onSelect={() => navigator.clipboard.writeText(row.original.objectName)}
                >
                  <DropdownMenuItemIcon>
                    <Copy />
                  </DropdownMenuItemIcon>
                  <DropdownMenuItemText>Copy name</DropdownMenuItemText>
                </DropdownMenuItem>
                <DropdownMenuSeparator />
                <DropdownMenuItem
                  data-analytics-id='SECURITY_ROW_FILTER'
                  data-analytics-props={`{"rowId":"${row.original.id}"}`}
                  onSelect={() => track('SECURITY_ROW_FILTER_APPLIED', { rowId: row.original.id })}
                >
                  <DropdownMenuItemIcon>
                    <Filter />
                  </DropdownMenuItemIcon>
                  <DropdownMenuItemText>Show only</DropdownMenuItemText>
                </DropdownMenuItem>
                <DropdownMenuItem
                  data-analytics-id='SECURITY_ROW_EXCLUDE'
                  data-analytics-props={`{"rowId":"${row.original.id}"}`}
                  onSelect={() => track('SECURITY_ROW_EXCLUDE_APPLIED', { rowId: row.original.id })}
                >
                  <DropdownMenuItemIcon>
                    <FilterX />
                  </DropdownMenuItemIcon>
                  <DropdownMenuItemText>Exclude</DropdownMenuItemText>
                </DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          ),
        },
        cell: ({ row }) => (
          <a
            href={`#/events/${row.original.id}`}
            data-analytics-id='SECURITY_ROW_OPEN'
            data-analytics-props={`{"rowId":"${row.original.id}"}`}
          >
            {row.original.objectName}
          </a>
        ),
      }),
      // Other sortable columns — each one declares its own sort target.
      securityColumnHelper.accessor('requests', {
        header: ({ column }) => (
          <TableSortTrigger column={column} data-analytics-id='SECURITY_SORT_REQUESTS'>
            Requests
          </TableSortTrigger>
        ),
        size: 140,
        enableSorting: true,
      }),
      securityColumnHelper.accessor('sourceIp', {
        header: ({ column }) => (
          <TableSortTrigger column={column} data-analytics-id='SECURITY_SORT_SOURCE_IP'>
            Source
          </TableSortTrigger>
        ),
        size: 180,
        enableSorting: true,
        meta: { sortType: 'text' as const },
      }),
      // Status column — demonstrates the column header menu seam with
      // per-item analytics, rendered as a sibling of the sort trigger inside
      // a Fragment.
      securityColumnHelper.accessor('status', {
        header: ({ column }) => (
          <>
            <TableSortTrigger column={column} data-analytics-id='SECURITY_SORT_STATUS'>
              Status
            </TableSortTrigger>
            <TableColumnMenu column={column} data-analytics-id='SECURITY_COLMENU_STATUS'>
              <TableColumnMenuSortItem
                direction='asc'
                data-analytics-id='SECURITY_COLMENU_STATUS_SORT_ASC'
              />
              <TableColumnMenuSortItem
                direction='desc'
                data-analytics-id='SECURITY_COLMENU_STATUS_SORT_DESC'
              />
              <TableColumnMenuPinItem data-analytics-id='SECURITY_COLMENU_STATUS_PIN' />
              <TableColumnMenuHideItem data-analytics-id='SECURITY_COLMENU_STATUS_HIDE' />
              <TableColumnMenuMoveLeftItem data-analytics-id='SECURITY_COLMENU_STATUS_MOVE_LEFT' />
              <TableColumnMenuMoveRightItem data-analytics-id='SECURITY_COLMENU_STATUS_MOVE_RIGHT' />
            </TableColumnMenu>
          </>
        ),
        size: 130,
        enableSorting: true,
        meta: { sortType: 'text' as const, resizeType: 'cut' as const },
      }),
      securityColumnHelper.accessor('firstDetected', {
        header: ({ column }) => (
          <TableSortTrigger column={column} data-analytics-id='SECURITY_SORT_FIRST_DETECTED'>
            First detected
          </TableSortTrigger>
        ),
        size: 160,
        enableSorting: true,
        meta: { sortType: 'date' as const, resizeType: 'cut' as const },
      }),
    ],
    [track],
  );

  return (
    <Table
      data-testid='security-events-table'
      data={securityEvents}
      columns={columnsWithAnalytics}
      getRowId={row => row.id}
      // Sort: DOM seam on the icon (TableSortTrigger) + callback for direction.
      sorting={sorting}
      onSortingChange={updater => {
        setSorting(prev => {
          const next = typeof updater === 'function' ? updater(prev) : updater;
          track('SECURITY_TABLE_SORT_CHANGED', { columnId: next[0]?.id, desc: !!next[0]?.desc });
          return next;
        });
      }}
      // Selection / expand / settings menu — no DOM seam yet; callbacks carry
      // the resulting state.
      rowSelection={rowSelection}
      onRowSelectionChange={updater => {
        setRowSelection(prev => {
          const next = typeof updater === 'function' ? updater(prev) : updater;
          track('SECURITY_TABLE_SELECTION_CHANGED', { selectedIds: Object.keys(next) });
          return next;
        });
      }}
      expanded={expanded}
      onExpandedChange={updater => {
        setExpanded(prev => {
          const next = typeof updater === 'function' ? updater(prev) : updater;
          track('SECURITY_TABLE_EXPAND_CHANGED', { state: next });
          return next;
        });
      }}
      columnVisibility={columnVisibility}
      onColumnVisibilityChange={updater => {
        setColumnVisibility(prev => {
          const next = typeof updater === 'function' ? updater(prev) : updater;
          track('SECURITY_TABLE_COLUMNS_VISIBILITY_CHANGED', next);
          return next;
        });
      }}
      columnOrder={columnOrder}
      onColumnOrderChange={updater => {
        setColumnOrder(prev => {
          const next = typeof updater === 'function' ? updater(prev) : updater;
          track('SECURITY_TABLE_COLUMNS_REORDERED', { order: next });
          return next;
        });
      }}
      columnPinning={columnPinning}
      onColumnPinningChange={updater => {
        setColumnPinning(prev => {
          const next = typeof updater === 'function' ? updater(prev) : updater;
          track('SECURITY_TABLE_COLUMNS_PINNED', next);
          return next;
        });
      }}
      columnSizing={columnSizing}
      onColumnSizingChange={updater => {
        setColumnSizing(prev => {
          const next = typeof updater === 'function' ? updater(prev) : updater;
          track('SECURITY_TABLE_COLUMN_RESIZED', next);
          return next;
        });
      }}
      onMasterCellClick={rowId => track('SECURITY_TABLE_MASTER_CELL_CLICKED', { rowId })}
      renderExpandedRow={row => (
        <div className='p-16 bg-states-primary-default-alt rounded-8'>
          <HStack justify='between' align='center'>
            <VStack gap={2}>
              <Text size='xs' color='secondary'>
                Source IP
              </Text>
              <Text size='sm'>{row.original.sourceIp}</Text>
            </VStack>
            <Button
              variant='outline'
              color='neutral'
              size='small'
              data-analytics-id='SECURITY_EXPANDED_VIEW_DETAILS'
              data-analytics-props={`{"rowId":"${row.original.id}"}`}
              onClick={() =>
                track('SECURITY_EXPANDED_VIEW_DETAILS_CLICKED', { rowId: row.original.id })
              }
            >
              View full details
            </Button>
          </HStack>
        </div>
      )}
    >
      <TableActionBar>
        {/*
          Selection summary — override the default block to attach analytics to
          the "Select all" / "Clear" controls. The DS keeps the wiring (toggle /
          reset, disabled, count); the consumer onClick runs first and feeds the
          same `track` stream (call preventDefault() to skip the DS action).
        */}
        <TableActionBarSelection>
          <BulkBarSummaryCount />
          <BulkBarSummarySelectAll
            data-analytics-id='SECURITY_BULK_SELECT_ALL'
            onClick={() => track('SECURITY_BULK_SELECT_ALL_CLICKED')}
          />
          <BulkBarSummarySeparator />
          <BulkBarSummaryClear
            data-analytics-id='SECURITY_BULK_CLEAR'
            onClick={() => track('SECURITY_BULK_CLEAR_CLICKED')}
          />
        </TableActionBarSelection>

        <Button
          variant='ghost'
          color='neutral-alt'
          data-analytics-id='SECURITY_BULK_DUPLICATE'
          data-analytics-props={`{"count":${Object.keys(rowSelection).length}}`}
          onClick={() =>
            track('SECURITY_BULK_DUPLICATE_CLICKED', {
              selectedIds: Object.keys(rowSelection),
            })
          }
        >
          <Copy /> Duplicate
        </Button>
        <Button
          color='brand'
          data-analytics-id='SECURITY_BULK_DELETE'
          data-analytics-props={`{"count":${Object.keys(rowSelection).length}}`}
          onClick={() =>
            track('SECURITY_BULK_DELETE_CLICKED', {
              selectedIds: Object.keys(rowSelection),
            })
          }
        >
          <Trash2 /> Delete
        </Button>
      </TableActionBar>

      <TableScrollHandler>
        <TableScrollHandlerLeft data-analytics-id='SECURITY_SCROLL_LEFT' />
        <TableScrollHandlerRight data-analytics-id='SECURITY_SCROLL_RIGHT' />
      </TableScrollHandler>
    </Table>
  );
};
```

### Loading state

`isLoading` with nothing to show yet: skeleton rows under a real header, so the columns do
not jump when the data lands.

```tsx
import { Table } from '@wallarm-org/design-system/Table';
import { securityColumns } from './mocks';

export const LoadingState = () => (
  <Table data={[]} columns={securityColumns} isLoading />
);
```

### Loading with data

The same flag with rows already present appends the skeletons below them — this is the shape
for fetching the next batch, not for replacing what is on screen.

```tsx
import { Table } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const LoadingWithData = () => (
  <Table
    className='h-500'
    data={securityEvents}
    columns={securityColumns}
    getRowId={row => row.id}
    isLoading
  />
);
```

### Empty collection

`TableEmptyState` holding an `EmptyState` of `type='collection-empty'`: nothing exists yet,
so the action creates the first thing.

```tsx
import { Button } from '@wallarm-org/design-system/Button';
import { EmptyState, EmptyStateActions, EmptyStateDescription, EmptyStateIllustration, EmptyStateMessage, EmptyStateTitle } from '@wallarm-org/design-system/EmptyState';
import { Database } from '@wallarm-org/design-system/icons';
import { Table, TableEmptyState } from '@wallarm-org/design-system/Table';
import { securityColumns } from './mocks';

export const EmptyCollection = () => (
  <Table data={[]} columns={securityColumns} getRowId={row => row.id}>
    <TableEmptyState>
      <EmptyState type='collection-empty'>
        <EmptyStateIllustration>
          <Database />
        </EmptyStateIllustration>
        <EmptyStateMessage>
          <EmptyStateTitle>No events yet</EmptyStateTitle>
          <EmptyStateDescription>
            Security events will appear here as soon as Wallarm detects traffic.
          </EmptyStateDescription>
        </EmptyStateMessage>
        <EmptyStateActions>
          <Button size='medium'>Configure source</Button>
        </EmptyStateActions>
      </EmptyState>
    </TableEmptyState>
  </Table>
);
```

### No results

When data exists but the current filters or search matched nothing, stay at
the `collection-empty` scale — a table body is a page-level surface, and
`no-results` is the compact treatment for a card or menu. What changes is the
action: a neutral one that clears the filters, never a primary "create".

```tsx
import { Button } from '@wallarm-org/design-system/Button';
import { EmptyState, EmptyStateActions, EmptyStateDescription, EmptyStateIllustration, EmptyStateMessage, EmptyStateTitle } from '@wallarm-org/design-system/EmptyState';
import { FilterX, SearchX } from '@wallarm-org/design-system/icons';
import { Table, TableEmptyState } from '@wallarm-org/design-system/Table';
import { securityColumns } from './mocks';

export const NoResults = () => (
  <Table data={[]} columns={securityColumns} getRowId={row => row.id}>
    <TableEmptyState>
      <EmptyState type='collection-empty'>
        <EmptyStateIllustration>
          <SearchX />
        </EmptyStateIllustration>
        <EmptyStateMessage>
          <EmptyStateTitle>No results found</EmptyStateTitle>
          <EmptyStateDescription>
            No events match your current filters. Try adjusting or resetting them.
          </EmptyStateDescription>
        </EmptyStateMessage>
        <EmptyStateActions>
          <Button size='medium' variant='outline' color='neutral'>
            <FilterX />
            Reset filters
          </Button>
        </EmptyStateActions>
      </EmptyState>
    </TableEmptyState>
  </Table>
);
```

### Column resizing

Drag a header edge to resize. `columnSizing` is state like everything else, so a width can
be remembered between visits.

```tsx
import { useState } from 'react';
import { Table, type TableColumnSizingState } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const ColumnResizing = () => {
  const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});

  return (
    <Table
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      columnSizing={columnSizing}
      onColumnSizingChange={setColumnSizing}
    />
  );
};
```

### Column resizing with overflow list

A resizable column whose cell holds an `OverflowList`: the tags re-measure as the column
narrows instead of being clipped by it.

```tsx
import { useMemo, useState } from 'react';
import { OverflowList } from '@wallarm-org/design-system/OverflowList';
import { Table, type TableColumnDef, type TableColumnSizingState } from '@wallarm-org/design-system/Table';
import { Tag } from '@wallarm-org/design-system/Tag';
import { type SecurityEvent, securityColumnHelper, securityColumns, securityEvents } from './mocks';

export const ColumnResizingWithOverflowList = () => {
  const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});

  const columns = useMemo<TableColumnDef<SecurityEvent>[]>(
    () => [
      ...securityColumns.slice(0, 1),
      securityColumnHelper.display({
        id: 'tags',
        header: 'Tags',
        size: 240,
        cell: ({ row }) => (
          <OverflowList
            data-testid='tags-overflow-list'
            className='gap-4'
            items={row.original.tags}
            itemRenderer={(item: string) => (
              <Tag key={item} data-testid='tag'>
                {item}
              </Tag>
            )}
          />
        ),
      }),
      ...securityColumns.slice(1),
    ],
    [],
  );

  return (
    <Table
      data={securityEvents}
      columns={columns}
      getRowId={row => row.id}
      columnSizing={columnSizing}
      onColumnSizingChange={setColumnSizing}
      data-testid='resizing-overflow-table'
    />
  );
};
```

### Column resizing with pinning

Resizing alongside a pinned column — the pinned one holds its position while the rest of the
row moves underneath it.

```tsx
import { useState } from 'react';
import { Table, type TableColumnPinningState, type TableColumnSizingState } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const ColumnResizingWithPinning = () => {
  const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});
  const [columnPinning, setColumnPinning] = useState<TableColumnPinningState>({
    left: ['objectName'],
  });

  return (
    <Table
      className='max-w-920'
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      columnSizing={columnSizing}
      onColumnSizingChange={setColumnSizing}
      columnPinning={columnPinning}
      onColumnPinningChange={setColumnPinning}
    />
  );
};
```

### Column pinning

`columnPinning` freezes a column to the left, so the name of the thing stays readable
however far sideways the table scrolls.

```tsx
import { useState } from 'react';
import { Table, type TableColumnPinningState } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const ColumnPinning = () => {
  const [columnPinning, setColumnPinning] = useState<TableColumnPinningState>({
    left: ['objectName'],
  });

  return (
    <Table
      className='max-w-920'
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      columnPinning={columnPinning}
      onColumnPinningChange={setColumnPinning}
    />
  );
};
```

### Column drag and drop

Drag a header to reorder the columns. `columnOrder` is the same state the settings menu
writes, so the two never disagree.

```tsx
import { useState } from 'react';
import { Table } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const ColumnDragAndDrop = () => {
  const [columnOrder, setColumnOrder] = useState<string[]>([]);

  return (
    <Table
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      columnOrder={columnOrder}
      onColumnOrderChange={setColumnOrder}
    />
  );
};
```

### Column header menu

The per-column menu that appears on hover — sort, pin, hide, move — which is where the
column controls live once a header is too narrow to show them.

```tsx
import { useState } from 'react';
import { Table, type TableColumnPinningState, type TableSortingState, type TableVisibilityState } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const ColumnHeaderMenu = () => {
  const [sorting, setSorting] = useState<TableSortingState>([]);
  const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});
  const [columnPinning, setColumnPinning] = useState<TableColumnPinningState>({});
  const [columnOrder, setColumnOrder] = useState<string[]>([]);

  return (
    <Table
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      sorting={sorting}
      onSortingChange={setSorting}
      columnVisibility={columnVisibility}
      onColumnVisibilityChange={setColumnVisibility}
      columnPinning={columnPinning}
      onColumnPinningChange={setColumnPinning}
      columnOrder={columnOrder}
      onColumnOrderChange={setColumnOrder}
    />
  );
};
```

### Row selection

Checkboxes plus `TableActionBar`, which rises from the bottom once a row is ticked and
carries the verbs that apply to the whole selection.

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Copy, Trash2 } from '@wallarm-org/design-system/icons';
import { Table, TableActionBar, type TableRowSelectionState } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const RowSelection = () => {
  const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});

  return (
    <Table
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      rowSelection={rowSelection}
      onRowSelectionChange={setRowSelection}
    >
      <TableActionBar>
        <Button variant='ghost' color='neutral-alt' onClick={onDuplicate}>
          <Copy /> Duplicate
        </Button>
        <Button color='brand' onClick={onDelete}>
          <Trash2 /> Delete
        </Button>
      </TableActionBar>
    </Table>
  );
};
```

### Select all rows gesture

`onSelectAllRows` separates the master checkbox from a hand-made full selection, which the
row state alone cannot tell apart. Here the gesture means "every row matching the filter",
including the ones the server has not sent — so the bar jumps to the filter's total, and any
manual change drops back to the rows actually loaded.

```tsx
import { useState } from 'react';
import { BulkBarSummaryClear, BulkBarSummaryCount } from '@wallarm-org/design-system/BulkBar';
import { Button } from '@wallarm-org/design-system/Button';
import { Trash2 } from '@wallarm-org/design-system/icons';
import { Table, TableActionBar, TableActionBarSelection, type TableRowSelectionState } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const SelectAllRowsGesture = () => {
  const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
  const [filterWide, setFilterWide] = useState(false);
  const MATCHING_THE_FILTER = 1474;

  const selectedCount = filterWide ? MATCHING_THE_FILTER : Object.keys(rowSelection).length;

  return (
    <Table
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      rowSelection={rowSelection}
      onRowSelectionChange={updater => {
        setRowSelection(updater);
        // "Everything matching the filter, minus this one row" is not a filter,
        // so any hand-made change falls back to the explicit list.
        setFilterWide(false);
      }}
      onSelectAllRows={setFilterWide}
    >
      <TableActionBar>
        <TableActionBarSelection>
          <BulkBarSummaryCount>
            {selectedCount} {selectedCount === 1 ? 'event' : 'events'} selected
          </BulkBarSummaryCount>
          <BulkBarSummaryClear />
        </TableActionBarSelection>
        <Button color='brand' onClick={onDelete}>
          <Trash2 /> Delete
        </Button>
      </TableActionBar>
    </Table>
  );
};
```

### Select all rows disabled

`enableSelectAllRows={false}` drops the master checkbox while keeping the column, for a
listing where "all rows" is not a scope the consumer can act on. Per-row checkboxes stay,
shift-click range selection included.

```tsx
import { useState } from 'react';
import { Table, type TableRowSelectionState } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const SelectAllRowsDisabled = () => {
  const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});

  return (
    <Table
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      rowSelection={rowSelection}
      onRowSelectionChange={setRowSelection}
      enableSelectAllRows={false}
    />
  );
};
```

### Row selection window scroll

A selection that outlives the table scrolling away: under `virtualized='window'` the bar is
pinned to the viewport rather than to the table.

```tsx
import { useMemo, useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Copy, Trash2 } from '@wallarm-org/design-system/icons';
import { Table, TableActionBar, type TableRowSelectionState } from '@wallarm-org/design-system/Table';
import { createLargeSecurityEvents, securityColumns } from './mocks';

export const RowSelectionWindowScroll = () => {
  const largeData = useMemo(() => createLargeSecurityEvents(100), []);
  const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});

  return (
    <>
      <Table
        data-testid='row-selection-window-scroll-table'
        data={largeData}
        columns={securityColumns}
        getRowId={row => row.id}
        virtualized='window'
        rowSelection={rowSelection}
        onRowSelectionChange={setRowSelection}
      >
        <TableActionBar>
          <Button variant='ghost' color='neutral-alt' onClick={onDuplicate}>
            <Copy /> Duplicate
          </Button>
          <Button color='brand' onClick={onDelete}>
            <Trash2 /> Delete
          </Button>
        </TableActionBar>
      </Table>
      {/* Filler so the e2e suite can scroll the table fully out of view while a
          selection stays active — this story's only content otherwise fills
          exactly to the table's own height, so max scroll would always leave
          the table's tail in view. Generous margin over the viewport height
          (800px in the e2e config) so the anchor is unambiguously out of view
          at max scroll, not just mostly. */}
      <div style={{ height: 3000 }} />
    </>
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--row-selection-window-scroll)

### Row grouping

`getSubRows` turns flat data into groups — each group row counts its children and collapses
them, keyed in the same `expanded` state a row detail uses.

```tsx
import { useState } from 'react';
import { Table, type TableExpandedState, type TableRowSelectionState, type TableSortingState } from '@wallarm-org/design-system/Table';
import { groupedHeaderData, headerColumns, type SecurityHeaderEntry } from './mocks';

export const RowGrouping = () => {
  const [expanded, setExpanded] = useState<TableExpandedState>({ 'group-cto': true });
  const [sorting, setSorting] = useState<TableSortingState>([{ id: 'lastEdited', desc: true }]);
  const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});

  return (
    <Table<SecurityHeaderEntry>
      data={groupedHeaderData}
      columns={headerColumns}
      getRowId={row => row.id}
      getSubRows={row => row.children}
      expanded={expanded}
      onExpandedChange={setExpanded}
      sorting={sorting}
      onSortingChange={setSorting}
      rowSelection={rowSelection}
      onRowSelectionChange={setRowSelection}
    />
  );
};
```

### Row expanding

`renderExpandedRow` draws a panel beneath the row, for the two or three fields that do not
deserve columns of their own.

```tsx
import { useState } from 'react';
import { HttpMethod } from '@wallarm-org/design-system/HttpMethod';
import { HStack, VStack } from '@wallarm-org/design-system/Stack';
import { Table, type TableExpandedState } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { securityColumns, securityEvents } from './mocks';

export const RowExpanding = () => {
  const [expanded, setExpanded] = useState<TableExpandedState>({});

  return (
    <Table
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      expanded={expanded}
      onExpandedChange={setExpanded}
      renderExpandedRow={row => (
        <div className='p-16 bg-states-primary-default-alt rounded-8'>
          <Text size='sm' weight='medium'>
            Event Details
          </Text>
          <div className='grid grid-cols-3 gap-8 mt-8'>
            <VStack gap={2}>
              <Text size='xs' color='secondary'>
                Source IP
              </Text>
              <Text size='sm'>{row.original.sourceIp}</Text>
            </VStack>
            <VStack gap={2}>
              <Text size='xs' color='secondary'>
                Provider
              </Text>
              <Text size='sm'>{row.original.sourceProvider}</Text>
            </VStack>
            <VStack gap={2}>
              <Text size='xs' color='secondary'>
                Endpoint
              </Text>
              <HStack gap={4}>
                <HttpMethod method={row.original.endpointMethod} />
                <Text size='sm'>{row.original.endpointPath}</Text>
              </HStack>
            </VStack>
          </div>
        </div>
      )}
    />
  );
};
```

### Context menu

Right-click inside a cell: `DropdownMenuContextTrigger` puts that column's verbs where the
pointer already is, so each column can offer its own.

```tsx
import { Badge } from '@wallarm-org/design-system/Badge';
import { InlineCodeSnippet } from '@wallarm-org/design-system/CodeSnippet';
import { DropdownMenu, DropdownMenuContent, DropdownMenuContextTrigger, DropdownMenuItem, DropdownMenuItemContent, DropdownMenuItemDescription, DropdownMenuItemIcon, DropdownMenuItemText, DropdownMenuSeparator } from '@wallarm-org/design-system/DropdownMenu';
import { Copy, Filter, FilterX } from '@wallarm-org/design-system/icons';
import { Table, type TableColumnDef } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { securityColumnHelper, securityEvents } from './mocks';

export const ContextMenu = () => {
  const contextMenuColumns: TableColumnDef<(typeof securityEvents)[number]>[] = [
    securityColumnHelper.accessor('objectName', {
      header: 'Object name',
      cell: ({ getValue }) => (
        <DropdownMenu>
          <DropdownMenuContextTrigger>
            <Text size='sm'>{getValue()}</Text>
          </DropdownMenuContextTrigger>
          <DropdownMenuContent>
            <DropdownMenuItem onSelect={onCopyValue}>
              <DropdownMenuItemIcon>
                <Copy />
              </DropdownMenuItemIcon>
              <DropdownMenuItemText>Copy value</DropdownMenuItemText>
            </DropdownMenuItem>

            <DropdownMenuSeparator />

            <DropdownMenuItem onSelect={onShowOnly}>
              <DropdownMenuItemIcon>
                <Filter />
              </DropdownMenuItemIcon>
              <DropdownMenuItemContent>
                <DropdownMenuItemText>Show only</DropdownMenuItemText>
                <DropdownMenuItemDescription>Filter to matching rows</DropdownMenuItemDescription>
              </DropdownMenuItemContent>
            </DropdownMenuItem>

            <DropdownMenuItem onSelect={onExclude}>
              <DropdownMenuItemIcon>
                <FilterX />
              </DropdownMenuItemIcon>
              <DropdownMenuItemContent>
                <DropdownMenuItemText>Exclude</DropdownMenuItemText>
                <DropdownMenuItemDescription>Filter out matching rows</DropdownMenuItemDescription>
              </DropdownMenuItemContent>
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      ),
    }),
    securityColumnHelper.accessor('parameter', {
      header: 'Parameters',
      cell: ({ getValue }) => (
        <DropdownMenu>
          <DropdownMenuContextTrigger>
            <InlineCodeSnippet code={getValue()} size='sm' copyable={false} />
          </DropdownMenuContextTrigger>
          <DropdownMenuContent>
            <DropdownMenuItem onSelect={onExecute}>
              <DropdownMenuItemIcon>
                <Copy />
              </DropdownMenuItemIcon>
              <DropdownMenuItemText>Execute</DropdownMenuItemText>
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      ),
    }),
    securityColumnHelper.accessor('status', {
      header: 'Status',
      cell: ({ getValue }) => {
        const status = getValue();
        return (
          <Badge
            variant='dotted'
            color={status === 'Blocked' ? 'red' : 'yellow'}
            type='secondary'
            size='medium'
          >
            {status}
          </Badge>
        );
      },
    }),
    securityColumnHelper.accessor('cweId', {
      header: 'Security info',
      cell: ({ getValue }) => (
        <Badge color='rose' type='secondary' size='medium' textVariant='code'>
          {getValue()}
        </Badge>
      ),
    }),
  ];

  return <Table data={securityEvents} columns={contextMenuColumns} getRowId={row => row.id} />;
};
```

### Settings menu

The gear at the end of the header — search the columns, show and hide them, reorder them,
reset — writing the same visibility and order state as the header menu.

```tsx
import { useState } from 'react';
import { Table, type TableVisibilityState } from '@wallarm-org/design-system/Table';
import { securityColumnIds, securityColumns, securityEvents } from './mocks';

export const SettingsMenu = () => {
  const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});
  const [columnOrder, setColumnOrder] = useState<string[]>([]);

  return (
    <Table
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      columnVisibility={columnVisibility}
      onColumnVisibilityChange={setColumnVisibility}
      columnOrder={columnOrder}
      onColumnOrderChange={setColumnOrder}
      defaultColumnVisibility={{}}
      defaultColumnOrder={securityColumnIds}
    />
  );
};
```

### Settings menu deferred

`deferColumnVisibility` holds switch toggles and "Reset to default" while the
settings menu is open and applies them as one `columnVisibility` change on
close — for consumers that fetch a column's data only once the menu closes,
so a new column never sits empty behind the open menu.

```tsx
import { useState } from 'react';
import { Table, type TableVisibilityState } from '@wallarm-org/design-system/Table';
import { securityColumnIds, securityColumns, securityEvents } from './mocks';

export const SettingsMenuDeferred = () => {
  const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});
  const [columnOrder, setColumnOrder] = useState<string[]>([]);

  return (
    <Table
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      columnVisibility={columnVisibility}
      onColumnVisibilityChange={setColumnVisibility}
      columnOrder={columnOrder}
      onColumnOrderChange={setColumnOrder}
      defaultColumnVisibility={{}}
      defaultColumnOrder={securityColumnIds}
      deferColumnVisibility
    />
  );
};
```

### Settings menu grouped

`columnGroups` turns the settings menu into labeled, non-reorderable sections
(the FilterInput field-menu pattern): no drag handle, columns rendered under
their group header in the given order. Column ids absent from every group
(`cweId` here) fall into a trailing headerless section.

```tsx
import { useState } from 'react';
import { Table, type TableColumnGroup, type TableVisibilityState } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const SettingsMenuGrouped = () => {
  const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});

  const columnGroups: TableColumnGroup[] = [
    {
      label: 'Threat classification',
      columns: ['objectName', 'status', 'firstDetected', 'lastSeen'],
    },
    { label: 'Request features', columns: ['sourceIp', 'parameter', 'endpoint', 'requests'] },
  ];

  return (
    <Table
      data={securityEvents}
      columns={securityColumns}
      getRowId={row => row.id}
      columnVisibility={columnVisibility}
      onColumnVisibilityChange={setColumnVisibility}
      columnGroups={columnGroups}
    />
  );
};
```

### Virtualization

`virtualized='container'` inside a fixed height renders only the rows in view, which is what
lets a thousand rows behave like ten.

```tsx
import { useMemo } from 'react';
import { Table } from '@wallarm-org/design-system/Table';
import { createLargeSecurityEvents, securityColumns } from './mocks';

export const Virtualization = () => {
  const largeData = useMemo(() => createLargeSecurityEvents(1000), []);

  return (
    <Table
      className='h-500'
      data={largeData}
      columns={securityColumns}
      getRowId={row => row.id}
      virtualized='container'
    />
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--virtualization)

### Window virtualization

`virtualized='window'` measures against the page's own scroll instead, for a table that is
the page rather than a panel inside one.

```tsx
import { useMemo, useState } from 'react';
import { Table, type TableColumnSizingState } from '@wallarm-org/design-system/Table';
import { createLargeSecurityEvents, securityColumns } from './mocks';

export const WindowVirtualization = () => {
  const largeData = useMemo(() => createLargeSecurityEvents(1000), []);
  const [columnOrder, setColumnOrder] = useState<string[]>([]);
  const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});

  return (
    <Table
      data={largeData}
      columns={securityColumns}
      getRowId={row => row.id}
      virtualized='window'
      columnOrder={columnOrder}
      onColumnOrderChange={setColumnOrder}
      columnSizing={columnSizing}
      onColumnSizingChange={setColumnSizing}
    />
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--window-virtualization)

### Scroll to row

The imperative escape hatch: `scrollToRow(id, { align })` off the ref, returning false for
an id the table does not have so a deep link can fall back.

```tsx
import { useMemo, useRef, useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { HStack, VStack } from '@wallarm-org/design-system/Stack';
import { Table, type TableHandle } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { createLargeSecurityEvents, securityColumns } from './mocks';

export const ScrollToRow = () => {
  const largeData = useMemo(() => createLargeSecurityEvents(1000), []);
  const tableRef = useRef<TableHandle>(null);
  const [lastResult, setLastResult] = useState<string | null>(null);

  const scroll = (index: number, align: 'start' | 'center' | 'end' | 'auto' = 'center') => {
    const row = largeData[index];
    if (!row) return;
    const ok = tableRef.current?.scrollToRow(row.id, { align, behavior: 'smooth' }) ?? false;
    setLastResult(`scrollToRow("${row.id}", { align: "${align}" }) → ${ok}`);
  };

  return (
    <VStack gap={8}>
      <HStack gap={8}>
        <Button variant='outline' color='neutral' onClick={() => scroll(0, 'start')}>
          Top
        </Button>
        <Button variant='outline' color='neutral' onClick={() => scroll(499, 'center')}>
          Middle
        </Button>
        <Button variant='outline' color='neutral' onClick={() => scroll(999, 'end')}>
          Bottom
        </Button>
        <Button
          variant='secondary'
          color='neutral'
          onClick={() => {
            const ok = tableRef.current?.scrollToRow('does-not-exist') ?? false;
            setLastResult(`scrollToRow("does-not-exist") → ${ok}`);
          }}
        >
          Missing id (returns false)
        </Button>
      </HStack>
      {lastResult && (
        <Text size='sm' color='secondary'>
          {lastResult}
        </Text>
      )}
      <Table
        ref={tableRef}
        className='h-500'
        data={largeData}
        columns={securityColumns}
        getRowId={row => row.id}
        virtualized='container'
      />
    </VStack>
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--scroll-to-row)

### Infinite scroll

`onEndReached` with `isLoading` — the house default for a long collection, where rows arrive
as you reach them and there is no page number to lose.

```tsx
import { VStack } from '@wallarm-org/design-system/Stack';
import { Table } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { securityColumns, useInfiniteData } from './mocks';

export const InfiniteScroll = () => {
  const { data, isFetching, hasMore, totalItems, fetchNextPage } = useInfiniteData();

  return (
    <VStack gap={8}>
      <Text size='sm' color='secondary'>
        Loaded {data.length} of {totalItems} rows {isFetching && '— loading...'}
        {!hasMore && ' — all loaded'}
      </Text>
      <Table
        className='h-500'
        data={data}
        columns={securityColumns}
        getRowId={row => row.id}
        virtualized='container'
        isLoading={isFetching}
        onEndReached={fetchNextPage}
        onEndReachedThreshold={200}
      />
    </VStack>
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--infinite-scroll)

### Infinite scroll window

The same fetch-on-approach against the page's scroll, where the trigger has to account for
everything else on the page as well as the table.

```tsx
import { VStack } from '@wallarm-org/design-system/Stack';
import { Table } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { securityColumns, useInfiniteData } from './mocks';

export const InfiniteScrollWindow = () => {
  const { data, isFetching, hasMore, totalItems, fetchNextPage } = useInfiniteData();

  return (
    <VStack gap={8}>
      <Text size='sm' color='secondary'>
        Loaded {data.length} of {totalItems} rows {isFetching && '— loading...'}
        {!hasMore && ' — all loaded'}
      </Text>
      <Table
        data={data}
        columns={securityColumns}
        getRowId={row => row.id}
        virtualized='window'
        isLoading={isFetching}
        onEndReached={fetchNextPage}
        onEndReachedThreshold={300}
      />
    </VStack>
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--infinite-scroll-window)

### Bidirectional infinite scroll

Both ends at once: `onStartReached` prepends, and the scroll position is compensated so the
row you were reading does not slide out from under you.

```tsx
import { VStack } from '@wallarm-org/design-system/Stack';
import { Table } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { securityColumns, useBidirectionalData } from './mocks';

export const BidirectionalInfiniteScroll = () => {
  const {
    data,
    anchorId,
    isFetchingPrev,
    isFetchingNext,
    hasPrev,
    hasNext,
    fetchPrevPage,
    fetchNextPage,
  } = useBidirectionalData();

  return (
    <VStack gap={8}>
      <Text size='sm' color='secondary'>
        Window of {data.length} rows around the anchor — scroll up for top skeletons, down for
        bottom ones{(isFetchingPrev || isFetchingNext) && ' — loading...'}
        {!hasPrev && ' — top reached'}
        {!hasNext && ' — bottom reached'}
      </Text>
      <Table
        className='h-500'
        data={data}
        columns={securityColumns}
        getRowId={row => row.id}
        virtualized='container'
        isLoading={isFetchingNext}
        isLoadingPrevious={isFetchingPrev}
        initialScrollToRowId={anchorId}
        onStartReached={fetchPrevPage}
        onStartReachedThreshold={200}
        onEndReached={fetchNextPage}
        onEndReachedThreshold={200}
      />
    </VStack>
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--bidirectional-infinite-scroll)

### Bidirectional infinite scroll window

The prepend case against the document scroll, where unrelated page content shares the height
being measured — the regression this story exists to catch.

```tsx
import { VStack } from '@wallarm-org/design-system/Stack';
import { Table } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { securityColumns, useBidirectionalData } from './mocks';

export const BidirectionalInfiniteScrollWindow = () => {
  const {
    data,
    anchorId,
    isFetchingPrev,
    isFetchingNext,
    hasPrev,
    hasNext,
    fetchPrevPage,
    fetchNextPage,
  } = useBidirectionalData();

  return (
    <VStack gap={8}>
      <Text size='sm' color='secondary'>
        Window of {data.length} rows around the anchor
        {(isFetchingPrev || isFetchingNext) && ' — loading...'}
        {!hasPrev && ' — top reached'}
        {!hasNext && ' — bottom reached'}
      </Text>
      <Table
        data={data}
        columns={securityColumns}
        getRowId={row => row.id}
        virtualized='window'
        isLoading={isFetchingNext}
        isLoadingPrevious={isFetchingPrev}
        initialScrollToRowId={anchorId}
        onStartReached={fetchPrevPage}
        onStartReachedThreshold={200}
        onEndReached={fetchNextPage}
        onEndReachedThreshold={200}
      />
    </VStack>
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--bidirectional-infinite-scroll-window)

### Infinite scroll window in pane

`virtualized='window'` in an app shell that scrolls a content pane instead of the document: the
table follows the pane. The `overflow-x: hidden` wrapper in between never scrolls (its
`overflow-y` only computes to `auto`), so it must not be taken for the scroll root.

```tsx
import { VStack } from '@wallarm-org/design-system/Stack';
import { Table } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { securityColumns, useBidirectionalData } from './mocks';

export const InfiniteScrollWindowInPane = () => {
  const { data, isFetchingPrev, isFetchingNext, hasPrev, hasNext, fetchPrevPage, fetchNextPage } =
    useBidirectionalData();

  return (
    <div data-testid='scroll-pane' style={{ height: 480, overflowY: 'auto' }}>
      <div style={{ overflowX: 'hidden' }}>
        <VStack gap={8}>
          <Text size='sm' color='secondary'>
            Window of {data.length} rows around the anchor
            {(isFetchingPrev || isFetchingNext) && ' — loading...'}
            {!hasPrev && ' — top reached'}
            {!hasNext && ' — bottom reached'}
          </Text>
          <Table
            data={data}
            columns={securityColumns}
            getRowId={row => row.id}
            virtualized='window'
            isLoading={isFetchingNext}
            isLoadingPrevious={isFetchingPrev}
            onStartReached={fetchPrevPage}
            onStartReachedThreshold={200}
            onEndReached={fetchNextPage}
            onEndReachedThreshold={200}
          />
        </VStack>
      </div>
    </div>
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--infinite-scroll-window-in-pane)

### Infinite scroll window in pane anchored

A share-link landing in a pane host: the window-mode table opens scrolled to its anchor row,
inside the pane — the document stays put.

```tsx
import { Table } from '@wallarm-org/design-system/Table';
import { securityColumns, useBidirectionalData } from './mocks';

export const InfiniteScrollWindowInPaneAnchored = () => {
  const { data, anchorId, isFetchingPrev, isFetchingNext, fetchPrevPage, fetchNextPage } =
    useBidirectionalData();

  return (
    <div data-testid='scroll-pane' style={{ height: 480, overflowY: 'auto' }}>
      <Table
        data={data}
        columns={securityColumns}
        getRowId={row => row.id}
        virtualized='window'
        isLoading={isFetchingNext}
        isLoadingPrevious={isFetchingPrev}
        initialScrollToRowId={anchorId}
        onStartReached={fetchPrevPage}
        onStartReachedThreshold={200}
        onEndReached={fetchNextPage}
        onEndReachedThreshold={200}
      />
    </div>
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--infinite-scroll-window-in-pane-anchored)

### Infinite scroll window in pane late rows

A pane host that mounts the table before its data: rows arrive after a delay. Neither edge may
fire against the window meanwhile — only the edge the user scrolls to.

```tsx
import { useEffect, useState } from 'react';
import { Table } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { securityColumns, useBidirectionalData } from './mocks';

export const InfiniteScrollWindowInPaneLateRows = () => {
  const { data, isFetchingPrev, isFetchingNext, fetchPrevPage, fetchNextPage } =
    useBidirectionalData();
  const [hasLoaded, setHasLoaded] = useState(false);
  const [calls, setCalls] = useState({ start: 0, end: 0 });
  useEffect(() => {
    const timer = setTimeout(() => setHasLoaded(true), 500);
    return () => clearTimeout(timer);
  }, []);

  return (
    <div data-testid='scroll-pane' style={{ height: 480, overflowY: 'auto' }}>
      <Text size='sm' color='secondary'>
        Edge calls: start {calls.start}, end {calls.end}
      </Text>
      <Table
        data={hasLoaded ? data : []}
        columns={securityColumns}
        getRowId={row => row.id}
        virtualized='window'
        isLoading={!hasLoaded || isFetchingNext}
        isLoadingPrevious={isFetchingPrev}
        // Nothing to page from before the first page — as with a real cursor.
        onStartReached={
          hasLoaded
            ? () => {
                setCalls(prev => ({ ...prev, start: prev.start + 1 }));
                fetchPrevPage();
              }
            : undefined
        }
        onStartReachedThreshold={200}
        onEndReached={
          hasLoaded
            ? () => {
                setCalls(prev => ({ ...prev, end: prev.end + 1 }));
                fetchNextPage();
              }
            : undefined
        }
        onEndReachedThreshold={200}
      />
    </div>
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--infinite-scroll-window-in-pane-late-rows)

### Infinite scroll window in hidden pane anchored

A share-link landing in a pane that starts hidden (a closed tab or drawer): the anchor waits
until the table is shown and laid out, then lands inside the pane.

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { VStack } from '@wallarm-org/design-system/Stack';
import { Table } from '@wallarm-org/design-system/Table';
import { securityColumns, useBidirectionalData } from './mocks';

export const InfiniteScrollWindowInHiddenPaneAnchored = () => {
  const [isShown, setIsShown] = useState(false);
  const { data, anchorId, isFetchingPrev, isFetchingNext, fetchPrevPage, fetchNextPage } =
    useBidirectionalData();

  return (
    <VStack gap={8}>
      <Button data-testid='show-table' onClick={() => setIsShown(true)}>
        Show table
      </Button>
      <div
        data-testid='scroll-pane'
        style={{ height: 480, overflowY: 'auto', display: isShown ? 'block' : 'none' }}
      >
        <Table
          data={data}
          columns={securityColumns}
          getRowId={row => row.id}
          virtualized='window'
          isLoading={isFetchingNext}
          isLoadingPrevious={isFetchingPrev}
          initialScrollToRowId={anchorId}
          onStartReached={fetchPrevPage}
          onStartReachedThreshold={200}
          onEndReached={fetchNextPage}
          onEndReachedThreshold={200}
        />
      </div>
    </VStack>
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--infinite-scroll-window-in-hidden-pane-anchored)

### Infinite scroll window in scrolled pane

A window-mode table mounting into a pane the user already scrolled (content above it): the pane
keeps its position — adopting the root must not reset it.

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { VStack } from '@wallarm-org/design-system/Stack';
import { Table } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { securityColumns, useBidirectionalData } from './mocks';

export const InfiniteScrollWindowInScrolledPane = () => {
  const [isShown, setIsShown] = useState(false);
  const { data, isFetchingNext, fetchNextPage } = useBidirectionalData();

  return (
    <VStack gap={8}>
      <Button data-testid='show-table' onClick={() => setIsShown(true)}>
        Show table
      </Button>
      <div data-testid='scroll-pane' style={{ height: 480, overflowY: 'auto' }}>
        <div style={{ height: 800 }}>
          <Text size='sm' color='secondary'>
            Content above the table
          </Text>
        </div>
        {isShown && (
          <Table
            data={data}
            columns={securityColumns}
            getRowId={row => row.id}
            virtualized='window'
            isLoading={isFetchingNext}
            onEndReached={fetchNextPage}
            onEndReachedThreshold={200}
          />
        )}
      </div>
    </VStack>
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--infinite-scroll-window-in-scrolled-pane)

### Header column description

A column can explain itself: `description` renders as a second line under the title, or as a
tooltip behind a dashed underline when the header has no room for one.

```tsx
import { useMemo, useState } from 'react';
import { Table, type TableColumnDef, type TableSortingState } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const HeaderColumnDescription = () => {
  const [sorting, setSorting] = useState<TableSortingState>([]);

  const columns = useMemo<TableColumnDef<(typeof securityEvents)[number]>[]>(
    () =>
      securityColumns.map(col => {
        const key = 'accessorKey' in col ? col.accessorKey : undefined;
        if (key === 'objectName') {
          return {
            ...col,
            meta: {
              ...col.meta,
              description: { type: 'text' as const, content: 'Target resource' },
            },
          };
        }
        if (key === 'sourceIp') {
          return {
            ...col,
            meta: {
              ...col.meta,
              description: { type: 'tooltip' as const, content: 'Request origin IP' },
            },
          };
        }
        if (key === 'requests') {
          return {
            ...col,
            meta: { ...col.meta, description: { type: 'tooltip' as const, content: 'Total hits' } },
          };
        }
        if (key === 'parameter') {
          return {
            ...col,
            meta: {
              ...col.meta,
              description: { type: 'text' as const, content: 'Affected param' },
            },
          };
        }
        return col;
      }),
    [],
  );

  return (
    <Table
      data={securityEvents}
      columns={columns}
      getRowId={row => row.id}
      sorting={sorting}
      onSortingChange={setSorting}
    />
  );
};
```

### Master cell with actions

The first column is the master cell — pinned, and the place per-row actions appear on hover,
so every row keeps its verbs in one predictable spot.

```tsx
import { useMemo, useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuItemIcon, DropdownMenuItemText, DropdownMenuSeparator, DropdownMenuTrigger } from '@wallarm-org/design-system/DropdownMenu';
import { Copy, Ellipsis, Filter, FilterX } from '@wallarm-org/design-system/icons';
import { Table, type TableColumnDef, type TableColumnSizingState, type TableSortingState } from '@wallarm-org/design-system/Table';
import { multiplySecurityEvents, type SecurityEvent, securityColumns } from './mocks';

export const MasterCellWithActions = () => {
  const [sorting, setSorting] = useState<TableSortingState>([]);
  const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});

  const data = useMemo(() => multiplySecurityEvents(), []);

  const columns = useMemo<TableColumnDef<SecurityEvent>[]>(
    () =>
      securityColumns.map((col, i) =>
        i === 0
          ? {
              ...col,
              meta: {
                ...col.meta,
                size: 400,
                resizeType: 'resize',
                renderMenuAction: (row: { original: SecurityEvent }) => (
                  <DropdownMenu>
                    <DropdownMenuTrigger asChild>
                      <Button variant='ghost' color='neutral' size='small' aria-label='More'>
                        <Ellipsis />
                      </Button>
                    </DropdownMenuTrigger>
                    <DropdownMenuContent>
                      <DropdownMenuItem
                        onSelect={() => navigator.clipboard.writeText(row.original.objectName)}
                      >
                        <DropdownMenuItemIcon>
                          <Copy />
                        </DropdownMenuItemIcon>
                        <DropdownMenuItemText>Copy name</DropdownMenuItemText>
                      </DropdownMenuItem>
                      <DropdownMenuSeparator />
                      <DropdownMenuItem onSelect={() => onShowOnly(row.original.objectName)}>
                        <DropdownMenuItemIcon>
                          <Filter />
                        </DropdownMenuItemIcon>
                        <DropdownMenuItemText>Show only</DropdownMenuItemText>
                      </DropdownMenuItem>
                      <DropdownMenuItem onSelect={() => onExclude(row.original.objectName)}>
                        <DropdownMenuItemIcon>
                          <FilterX />
                        </DropdownMenuItemIcon>
                        <DropdownMenuItemText>Exclude</DropdownMenuItemText>
                      </DropdownMenuItem>
                    </DropdownMenuContent>
                  </DropdownMenu>
                ),
              },
            }
          : col,
      ),
    [],
  );

  return (
    <Table
      className='max-w-920'
      data={data}
      columns={columns}
      getRowId={row => row.id}
      sorting={sorting}
      onSortingChange={setSorting}
      columnSizing={columnSizing}
      onColumnSizingChange={setColumnSizing}
    />
  );
};
```

### Master cell with drawer

`onMasterCellClick` with `activeRowId`: the master cell opens a `Drawer` and the row it
belongs to stays marked while the panel is open.

```tsx
import { useCallback, useMemo, useState } from 'react';
import { Drawer, DrawerBody, DrawerContent, DrawerHeader } from '@wallarm-org/design-system/Drawer';
import { Table, type TableColumnDef, type TableSortingState } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { renderSecurityPreviewContent, renderSecurityPreviewHeader, type SecurityEvent, securityColumns, securityEvents } from './mocks';

export const MasterCellWithDrawer = () => {
  const [sorting, setSorting] = useState<TableSortingState>([]);
  const [activeRowId, setActiveRowId] = useState<string | null>(null);

  const data = useMemo(
    () =>
      Array.from({ length: 4 }, (_, batch) =>
        securityEvents.map(row => ({
          ...row,
          id: `${row.id}-${batch}`,
          objectName: batch === 0 ? row.objectName : `${row.objectName} (${batch + 1})`,
        })),
      ).flat(),
    [],
  );

  const columns = useMemo<TableColumnDef<SecurityEvent>[]>(
    () =>
      securityColumns.map((col, i) =>
        i === 0
          ? {
              ...col,
              cell: ({ row }: { row: { original: SecurityEvent } }) => (
                <Text size='xs'>{row.original.objectName}</Text>
              ),
            }
          : col,
      ),
    [],
  );

  const handleMasterCellClick = useCallback((rowId: string) => {
    setActiveRowId(prev => (prev === rowId ? null : rowId));
  }, []);

  const activeRow = useMemo(() => data.find(d => d.id === activeRowId), [data, activeRowId]);

  return (
    <>
      <Table
        className='max-w-920'
        data={data}
        columns={columns}
        getRowId={row => row.id}
        sorting={sorting}
        onSortingChange={setSorting}
        onMasterCellClick={handleMasterCellClick}
        activeRowId={activeRowId}
      />
      <Drawer
        open={!!activeRow}
        onOpenChange={open => {
          if (!open) setActiveRowId(null);
        }}
        modal={false}
        overlay={false}
        closeOnOutsideClick={false}
        width={960}
      >
        <DrawerContent>
          {activeRow ? (
            renderSecurityPreviewHeader({ original: activeRow })
          ) : (
            <DrawerHeader>
              <span />
            </DrawerHeader>
          )}
          <DrawerBody>
            {activeRow && renderSecurityPreviewContent({ original: activeRow })}
          </DrawerBody>
        </DrawerContent>
      </Drawer>
    </>
  );
};
```

### Master cell with actions and drawer

Master cell with both an action menu and a drawer:
clicking the cell body opens the drawer, clicking the action button opens the menu
without triggering the drawer.

```tsx
import { useCallback, useMemo, useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Drawer, DrawerBody, DrawerContent, DrawerHeader } from '@wallarm-org/design-system/Drawer';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuItemIcon, DropdownMenuItemText, DropdownMenuSeparator, DropdownMenuTrigger } from '@wallarm-org/design-system/DropdownMenu';
import { Copy, Ellipsis, Filter, FilterX } from '@wallarm-org/design-system/icons';
import { Table, type TableColumnDef, type TableSortingState } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { multiplySecurityEvents, renderSecurityPreviewContent, renderSecurityPreviewHeader, type SecurityEvent, securityColumns } from './mocks';

export const MasterCellWithActionsAndDrawer = () => {
  const [sorting, setSorting] = useState<TableSortingState>([]);
  const [activeRowId, setActiveRowId] = useState<string | null>(null);

  const data = useMemo(() => multiplySecurityEvents(), []);

  const columns = useMemo<TableColumnDef<SecurityEvent>[]>(
    () =>
      securityColumns.map((col, i) =>
        i === 0
          ? {
              ...col,
              cell: ({ row }: { row: { original: SecurityEvent } }) => (
                <Text size='xs'>{row.original.objectName}</Text>
              ),
              meta: {
                ...col.meta,
                resizeType: 'resize',
                renderMenuAction: (row: { original: SecurityEvent }) => (
                  <DropdownMenu>
                    <DropdownMenuTrigger asChild>
                      <Button variant='ghost' color='neutral' size='small' aria-label='More'>
                        <Ellipsis />
                      </Button>
                    </DropdownMenuTrigger>
                    <DropdownMenuContent>
                      <DropdownMenuItem
                        onSelect={() => navigator.clipboard.writeText(row.original.objectName)}
                      >
                        <DropdownMenuItemIcon>
                          <Copy />
                        </DropdownMenuItemIcon>
                        <DropdownMenuItemText>Copy name</DropdownMenuItemText>
                      </DropdownMenuItem>
                      <DropdownMenuSeparator />
                      <DropdownMenuItem onSelect={() => onShowOnly(row.original.objectName)}>
                        <DropdownMenuItemIcon>
                          <Filter />
                        </DropdownMenuItemIcon>
                        <DropdownMenuItemText>Show only</DropdownMenuItemText>
                      </DropdownMenuItem>
                      <DropdownMenuItem onSelect={() => onExclude(row.original.objectName)}>
                        <DropdownMenuItemIcon>
                          <FilterX />
                        </DropdownMenuItemIcon>
                        <DropdownMenuItemText>Exclude</DropdownMenuItemText>
                      </DropdownMenuItem>
                    </DropdownMenuContent>
                  </DropdownMenu>
                ),
              },
            }
          : col,
      ),
    [],
  );

  const handleMasterCellClick = useCallback((rowId: string) => {
    setActiveRowId(prev => (prev === rowId ? null : rowId));
  }, []);

  const activeRow = useMemo(() => data.find(d => d.id === activeRowId), [data, activeRowId]);

  return (
    <>
      <Table
        className='max-w-920'
        data={data}
        columns={columns}
        getRowId={row => row.id}
        sorting={sorting}
        onSortingChange={setSorting}
        onMasterCellClick={handleMasterCellClick}
        activeRowId={activeRowId}
      />
      <Drawer
        open={!!activeRow}
        onOpenChange={open => {
          if (!open) setActiveRowId(null);
        }}
        modal={false}
        overlay={false}
        closeOnOutsideClick={false}
        width={960}
      >
        <DrawerContent>
          {activeRow ? (
            renderSecurityPreviewHeader({ original: activeRow })
          ) : (
            <DrawerHeader>
              <span />
            </DrawerHeader>
          )}
          <DrawerBody>
            {activeRow && renderSecurityPreviewContent({ original: activeRow })}
          </DrawerBody>
        </DrawerContent>
      </Drawer>
    </>
  );
};
```

### Full featured

Grouping, expansion, selection, pinning, virtualization and the action bar in one table —
the honest test of whether the features cooperate rather than merely exist.

```tsx
import { useMemo, useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Copy, Trash2 } from '@wallarm-org/design-system/icons';
import { Table, TableActionBar, type TableColumnPinningState, type TableColumnSizingState, type TableExpandedState, type TableRowSelectionState, type TableSortingState, type TableVisibilityState } from '@wallarm-org/design-system/Table';
import { createLargeGroupedData, fullFeaturedColumns, headerColumnIds, type SecurityHeaderEntry } from './mocks';

export const FullFeatured = () => {
  const data = useMemo(() => createLargeGroupedData(12, 50), []);

  const [sorting, setSorting] = useState<TableSortingState>([{ id: 'lastEdited', desc: true }]);
  const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
  const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});
  const [columnPinning, setColumnPinning] = useState<TableColumnPinningState>({
    left: ['objectName'],
  });
  const [columnOrder, setColumnOrder] = useState<string[]>([]);
  const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});
  const [expanded, setExpanded] = useState<TableExpandedState>({
    'group-0': true,
    'group-1': true,
  });

  return (
    <Table<SecurityHeaderEntry>
      className='h-500'
      data={data}
      columns={fullFeaturedColumns}
      getRowId={row => row.id}
      getSubRows={row => row.children}
      // Grouping
      expanded={expanded}
      onExpandedChange={setExpanded}
      // Sorting
      sorting={sorting}
      onSortingChange={setSorting}
      // Row selection
      rowSelection={rowSelection}
      onRowSelectionChange={setRowSelection}
      // Column resizing
      columnSizing={columnSizing}
      onColumnSizingChange={setColumnSizing}
      // Column pinning
      columnPinning={columnPinning}
      onColumnPinningChange={setColumnPinning}
      // Column DnD ordering
      columnOrder={columnOrder}
      onColumnOrderChange={setColumnOrder}
      // Column visibility + settings
      columnVisibility={columnVisibility}
      onColumnVisibilityChange={setColumnVisibility}
      defaultColumnVisibility={{}}
      defaultColumnOrder={headerColumnIds}
      // Virtual scrolling
      virtualized='container'
    >
      <TableActionBar>
        <Button variant='ghost' color='neutral-alt' onClick={onDuplicate}>
          <Copy /> Duplicate
        </Button>
        <Button color='brand' onClick={onDelete}>
          <Trash2 /> Delete
        </Button>
      </TableActionBar>
    </Table>
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--full-featured)

### Row reordering

`EditableTextCell` and `EditableSelectCell` in the plain columns, with the master column
deliberately read-only — editing in place suits a value the user owns, not one the system
reports. Hovering highlights the whole cell, clicking opens it without the content shifting,
and Enter or blur commits while Escape reverts.

```tsx
import { useCallback, useState } from 'react';
import { arrayMove } from '@dnd-kit/sortable';
import { Table, type TableRowReorderEvent } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const RowReordering = () => {
  const [data, setData] = useState(() => securityEvents.slice(0, 6));

  const handleRowReorder = useCallback((event: TableRowReorderEvent) => {
    setData(prev => {
      const oldIndex = prev.findIndex(row => row.id === event.activeRowId);
      const newIndex = prev.findIndex(row => row.id === event.overRowId);
      if (oldIndex === -1 || newIndex === -1) return prev;
      return arrayMove(prev, oldIndex, newIndex);
    });
  }, []);

  return (
    <Table
      data={data}
      columns={securityColumns}
      getRowId={row => row.id}
      onRowReorder={handleRowReorder}
      data-testid='row-reorder-table'
    />
  );
};
```

### Row reordering in groups

With `getSubRows` grouping, a row moves only among the rows of its own group; group rows stay
put. `showEmptyGroups` keeps a group whose `getSubRows` returns `[]` on screen as a group row.

```tsx
import { useCallback, useState } from 'react';
import { arrayMove } from '@dnd-kit/sortable';
import { Table, type TableExpandedState, type TableRowReorderEvent } from '@wallarm-org/design-system/Table';
import { groupedHeaderData, headerColumns, type SecurityHeaderEntry } from './mocks';

export const RowReorderingInGroups = () => {
  const [data, setData] = useState<SecurityHeaderEntry[]>(() => [
    ...groupedHeaderData,
    { ...groupedHeaderData[0]!, id: 'group-empty', objectName: 'Nothing here yet', children: [] },
  ]);
  const [expanded, setExpanded] = useState<TableExpandedState>(true);

  const handleRowReorder = useCallback((event: TableRowReorderEvent) => {
    setData(prev =>
      prev.map(group => {
        const children = group.children ?? [];
        const oldIndex = children.findIndex(row => row.id === event.activeRowId);
        const newIndex = children.findIndex(row => row.id === event.overRowId);
        if (oldIndex === -1 || newIndex === -1) return group;
        return { ...group, children: arrayMove(children, oldIndex, newIndex) };
      }),
    );
  }, []);

  return (
    <Table<SecurityHeaderEntry>
      data={data}
      columns={headerColumns}
      getRowId={row => row.id}
      getSubRows={row => row.children}
      showEmptyGroups
      expanded={expanded}
      onExpandedChange={setExpanded}
      onRowReorder={handleRowReorder}
      data-testid='row-reorder-groups-table'
    />
  );
};
```

### Grouped with full width rows

`groupRowSpan: 'full'` draws `renderGroupRow` across every data column — title on the left,
actions on the right edge; the content owns the collapse toggle. `renderFullWidthRow` turns a
regular row into a single spanning banner, and `getRowCanDrag` keeps the banner and the locked
first header in place while the other rows of the group reorder around them.

```tsx
import { useCallback, useState } from 'react';
import { arrayMove } from '@dnd-kit/sortable';
import { Badge } from '@wallarm-org/design-system/Badge';
import { Button } from '@wallarm-org/design-system/Button';
import { ChevronDown, ChevronRight, Ellipsis, Plus, Shield } from '@wallarm-org/design-system/icons';
import { HStack } from '@wallarm-org/design-system/Stack';
import { Table, type TableExpandedState, type TableRowReorderEvent } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { groupedHeaderData, headerColumns, type SecurityHeaderEntry } from './mocks';

export const GroupedWithFullWidthRows = () => {
  const [data, setData] = useState<SecurityHeaderEntry[]>(() => [
    {
      ...groupedHeaderData[0]!,
      children: [
        { ...groupedHeaderData[0]!, id: `${BANNER_ROW_PREFIX}csp`, children: undefined },
        ...(groupedHeaderData[0]!.children ?? []),
      ],
    },
    ...groupedHeaderData.slice(1, 3),
    { ...groupedHeaderData[0]!, id: 'group-empty', objectName: 'Nothing here yet', children: [] },
  ]);
  const [expanded, setExpanded] = useState<TableExpandedState>(true);

  const handleRowReorder = useCallback((event: TableRowReorderEvent) => {
    setData(prev =>
      prev.map(group => {
        const children = group.children ?? [];
        const oldIndex = children.findIndex(row => row.id === event.activeRowId);
        const newIndex = children.findIndex(row => row.id === event.overRowId);
        if (oldIndex === -1 || newIndex === -1) return group;
        return { ...group, children: arrayMove(children, oldIndex, newIndex) };
      }),
    );
  }, []);

  return (
    <Table<SecurityHeaderEntry>
      data={data}
      columns={headerColumns}
      getRowId={row => row.id}
      getSubRows={row => row.children}
      showEmptyGroups
      expanded={expanded}
      onExpandedChange={setExpanded}
      onRowReorder={handleRowReorder}
      groupRowSpan='full'
      renderGroupRow={row => {
        const isExpanded = row.getIsExpanded();
        return (
          <HStack gap={8} align='center'>
            <Button
              variant='ghost'
              color='neutral'
              size='small'
              aria-expanded={isExpanded}
              aria-label={isExpanded ? 'Collapse group' : 'Expand group'}
              onClick={row.getToggleExpandedHandler()}
            >
              {isExpanded ? <ChevronDown /> : <ChevronRight />}
            </Button>
            <Text weight='medium'>{row.original.objectName}</Text>
            <Badge color='gray' type='secondary' size='medium'>
              {row.subRows.filter(r => !r.id.startsWith(BANNER_ROW_PREFIX)).length}
            </Badge>
            <HStack gap={4} className='ml-auto'>
              <Button variant='ghost' color='neutral' size='small'>
                <Plus />
                Add header
              </Button>
              <Button variant='ghost' color='neutral' size='small' aria-label='More actions'>
                <Ellipsis />
              </Button>
            </HStack>
          </HStack>
        );
      }}
      renderFullWidthRow={row =>
        row.id.startsWith(BANNER_ROW_PREFIX) ? (
          <HStack gap={8} align='center'>
            <Shield size='sm' />
            <Text size='sm' weight='medium'>
              Wallarm baseline headers
            </Text>
            <Text size='sm' color='secondary'>
              · applied first · built-in, always on
            </Text>
            <Button variant='ghost' color='neutral' size='small' className='ml-auto'>
              Manage baseline →
            </Button>
          </HStack>
        ) : undefined
      }
      getRowCanDrag={row => !row.id.startsWith(BANNER_ROW_PREFIX) && !LOCKED_ROW_IDS.has(row.id)}
      data-testid='grouped-full-width-table'
    />
  );
};
```

### Grouped with expand column

`groupExpandColumn` keeps the expand column in grouping mode: the toggle sits in its own column,
first, and every row of a group — drag handle included — sits to the right of it, while
`renderGroupRow` keeps the title and the actions.

```tsx
import { useCallback, useState } from 'react';
import { arrayMove } from '@dnd-kit/sortable';
import { Badge } from '@wallarm-org/design-system/Badge';
import { Button } from '@wallarm-org/design-system/Button';
import { Plus } from '@wallarm-org/design-system/icons';
import { HStack } from '@wallarm-org/design-system/Stack';
import { Table, type TableExpandedState, type TableRowReorderEvent } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { groupedHeaderData, headerColumns, type SecurityHeaderEntry } from './mocks';

export const GroupedWithExpandColumn = () => {
  const [data, setData] = useState<SecurityHeaderEntry[]>(() => [
    ...groupedHeaderData,
    { ...groupedHeaderData[0]!, id: 'group-empty', objectName: 'Nothing here yet', children: [] },
  ]);
  const [expanded, setExpanded] = useState<TableExpandedState>(true);

  const handleRowReorder = useCallback((event: TableRowReorderEvent) => {
    setData(prev =>
      prev.map(group => {
        const children = group.children ?? [];
        const oldIndex = children.findIndex(row => row.id === event.activeRowId);
        const newIndex = children.findIndex(row => row.id === event.overRowId);
        if (oldIndex === -1 || newIndex === -1) return group;
        return { ...group, children: arrayMove(children, oldIndex, newIndex) };
      }),
    );
  }, []);

  return (
    <Table<SecurityHeaderEntry>
      data={data}
      columns={headerColumns}
      getRowId={row => row.id}
      getSubRows={row => row.children}
      showEmptyGroups
      expanded={expanded}
      onExpandedChange={setExpanded}
      onRowReorder={handleRowReorder}
      groupRowSpan='full'
      groupExpandColumn
      renderGroupRow={row => (
        <HStack gap={8} align='center'>
          <Text weight='medium'>{row.original.objectName}</Text>
          <Badge color='gray' type='secondary' size='medium'>
            {row.subRows.length}
          </Badge>
          <Button variant='ghost' color='neutral' size='small' className='ml-auto'>
            <Plus />
            Add header
          </Button>
        </HStack>
      )}
      data-testid='grouped-expand-column-table'
    />
  );
};
```

### Row reordering with selection

Row reordering coexists with row selection — each feature has its own column and they
don't interfere with each other.

```tsx
import { useCallback, useState } from 'react';
import { arrayMove } from '@dnd-kit/sortable';
import { Table, type TableRowReorderEvent, type TableRowSelectionState } from '@wallarm-org/design-system/Table';
import { securityColumns, securityEvents } from './mocks';

export const RowReorderingWithSelection = () => {
  const [data, setData] = useState(() => securityEvents.slice(0, 6));
  const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});

  const handleRowReorder = useCallback((event: TableRowReorderEvent) => {
    setData(prev => {
      const oldIndex = prev.findIndex(row => row.id === event.activeRowId);
      const newIndex = prev.findIndex(row => row.id === event.overRowId);
      if (oldIndex === -1 || newIndex === -1) return prev;
      return arrayMove(prev, oldIndex, newIndex);
    });
  }, []);

  return (
    <Table
      data={data}
      columns={securityColumns}
      getRowId={row => row.id}
      onRowReorder={handleRowReorder}
      rowSelection={rowSelection}
      onRowSelectionChange={setRowSelection}
    />
  );
};
```

### Row reordering virtualized

Row drag-and-drop with container virtualization — the `DragOverlay` renders the dragged row
outside the virtualizer so it stays visible even when scrolled out of the virtual window.

```tsx
import { useCallback, useState } from 'react';
import { arrayMove } from '@dnd-kit/sortable';
import { Table, type TableRowReorderEvent } from '@wallarm-org/design-system/Table';
import { createLargeSecurityEvents, securityColumns } from './mocks';

export const RowReorderingVirtualized = () => {
  const [data, setData] = useState(() => createLargeSecurityEvents(100));

  const handleRowReorder = useCallback((event: TableRowReorderEvent) => {
    setData(prev => {
      const oldIndex = prev.findIndex(row => row.id === event.activeRowId);
      const newIndex = prev.findIndex(row => row.id === event.overRowId);
      if (oldIndex === -1 || newIndex === -1) return prev;
      return arrayMove(prev, oldIndex, newIndex);
    });
  }, []);

  return (
    <Table
      data={data}
      columns={securityColumns}
      getRowId={row => row.id}
      onRowReorder={handleRowReorder}
      virtualized='container'
      estimateRowHeight={() => 40}
    />
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--row-reordering-virtualized)

### Inline cell editing

```tsx
import { useCallback, useMemo, useState } from 'react';
import { InlineCodeSnippet } from '@wallarm-org/design-system/CodeSnippet';
import { VStack } from '@wallarm-org/design-system/Stack';
import { EDITABLE_CELL_COLUMN_META, EditableSelectCell, EditableTextCell, Table, type TableColumnDef } from '@wallarm-org/design-system/Table';
import { Text } from '@wallarm-org/design-system/Text';
import { type SecurityEvent, securityColumnHelper, securityEvents } from './mocks';

export const InlineCellEditing = () => {
  const [data, setData] = useState<SecurityEvent[]>(() => securityEvents.slice(0, 6));
  // Category starts unselected for every row, so the select shows its placeholder.
  const [categories, setCategories] = useState<Record<string, string>>({});
  const updateCell = useCallback(
    <K extends keyof SecurityEvent>(rowId: string, key: K, value: SecurityEvent[K]) => {
      setData(prev => prev.map(row => (row.id === rowId ? { ...row, [key]: value } : row)));
    },
    [],
  );
  const columns = useMemo<TableColumnDef<SecurityEvent>[]>(
    () => [
      // The first column is the Table's pinned "master" column — keep it
      // read-only; editable cells live in the plain columns after it.
      securityColumnHelper.accessor('sourceIp', {
        header: 'Source',
        size: 200,
        meta: { resizeType: 'cut' as const },
        cell: ({ getValue }) => <Text size='sm'>{getValue()}</Text>,
      }),
      securityColumnHelper.accessor('objectName', {
        header: 'Object name',
        size: 400,
        meta: EDITABLE_CELL_COLUMN_META,
        cell: ({ row, getValue }) => (
          <EditableTextCell
            data-testid='editable-text-cell'
            value={getValue()}
            aria-label='Edit object name'
            onCommit={value => updateCell(row.id, 'objectName', value)}
          />
        ),
      }),
      securityColumnHelper.accessor('status', {
        header: 'Status',
        size: 200,
        meta: EDITABLE_CELL_COLUMN_META,
        cell: ({ row, getValue }) => (
          <EditableSelectCell
            data-testid='editable-select-cell'
            value={getValue()}
            items={statusItems}
            onCommit={value => updateCell(row.id, 'status', value as SecurityEvent['status'])}
          >
            <StatusBadge value={getValue()} />
          </EditableSelectCell>
        ),
      }),
      // Empty-by-default select: consumer supplies the `placeholder` text.
      securityColumnHelper.display({
        id: 'category',
        header: 'Category',
        size: 200,
        meta: EDITABLE_CELL_COLUMN_META,
        cell: ({ row }) => {
          const value = categories[row.id] ?? '';
          return (
            <EditableSelectCell
              value={value}
              items={categoryItems}
              placeholder='Select'
              aria-label='Edit category'
              onCommit={next => setCategories(prev => ({ ...prev, [row.id]: next }))}
            >
              {categoryItems.find(item => item.value === value)?.label}
            </EditableSelectCell>
          );
        },
      }),
      securityColumnHelper.accessor('parameter', {
        header: 'Parameters',
        size: 240,
        meta: { resizeType: 'cut' as const },
        cell: ({ getValue }) => <InlineCodeSnippet code={getValue()} size='sm' copyable={false} />,
      }),
    ],
    [updateCell, categories],
  );
  return (
    <VStack gap={12} align='stretch'>
      <Table data={data} columns={columns} getRowId={row => row.id} />
    </VStack>
  );
};
```

### Sticky group parent story

The group parent row sticks below the header while its children scroll — scroll through
a group to see the parent pin in place, then release when the group boundary passes.

```tsx
import { useMemo, useState } from 'react';
import { Table, type TableExpandedState, type TableRowSelectionState, type TableSortingState } from '@wallarm-org/design-system/Table';
import { createLargeGroupedData, headerColumns, type SecurityHeaderEntry } from './mocks';

export const StickyGroupParentStory = () => {
  const data = useMemo(() => createLargeGroupedData(4, 20), []);
  const [expanded, setExpanded] = useState<TableExpandedState>({
    'group-0': true,
    'group-1': true,
    'group-2': true,
    'group-3': true,
  });
  const [sorting, setSorting] = useState<TableSortingState>([{ id: 'lastEdited', desc: true }]);
  const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});

  return (
    <Table<SecurityHeaderEntry>
      className='h-300'
      data={data}
      columns={headerColumns}
      getRowId={row => row.id}
      getSubRows={row => row.children}
      expanded={expanded}
      onExpandedChange={setExpanded}
      sorting={sorting}
      onSortingChange={setSorting}
      rowSelection={rowSelection}
      onRowSelectionChange={setRowSelection}
      data-testid='sticky-group-parent-table'
    />
  );
};
```

### Sticky group parent virtualized

The same sticky-parent behavior under container virtualization with large grouped data —
the overlay stays in the DOM even when the real parent row is unmounted by the virtualizer.

```tsx
import { useMemo, useState } from 'react';
import { Table, type TableExpandedState, type TableRowSelectionState, type TableSortingState } from '@wallarm-org/design-system/Table';
import { createLargeGroupedData, headerColumns, type SecurityHeaderEntry } from './mocks';

export const StickyGroupParentVirtualized = () => {
  const data = useMemo(() => createLargeGroupedData(12, 50), []);
  const [expanded, setExpanded] = useState<TableExpandedState>({
    'group-0': true,
    'group-1': true,
    'group-2': true,
  });
  const [sorting, setSorting] = useState<TableSortingState>([{ id: 'lastEdited', desc: true }]);
  const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});

  return (
    <Table<SecurityHeaderEntry>
      className='h-500'
      data={data}
      columns={headerColumns}
      getRowId={row => row.id}
      getSubRows={row => row.children}
      expanded={expanded}
      onExpandedChange={setExpanded}
      sorting={sorting}
      onSortingChange={setSorting}
      rowSelection={rowSelection}
      onRowSelectionChange={setRowSelection}
      virtualized='container'
      data-testid='sticky-group-parent-virt-table'
    />
  );
};
```

[Open in Storybook](https://wallarm.github.io/design-system/storybook/?path=/story/data-display-table--sticky-group-parent-virtualized)

## Props

| Prop                     | Type                                                   | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------------------ | ------------------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| data \*                  | `T[]`                                                  |         | Data array for rows                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| columns \*               | `TableColumnDef<T>[]`                                  |         | Column definitions                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| isLoading                | `boolean \| undefined`                                 | `false` | Show skeleton rows                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| isLoadingPrevious        | `boolean \| undefined`                                 |         | Show skeleton rows above the first row while a previous page is being fetched (bidirectional infinite scroll — the start-edge counterpart of `isLoading`). Pair with `onStartReached`.                                                                                                                                                                                                                                                                             |
| skeletonCount            | `number \| undefined`                                  |         | Number of skeleton rows to display when loading (default: 6)                                                                                                                                                                                                                                                                                                                                                                                                       |
| getRowId                 | `((row: T, index: number) => string) \| undefined`     |         | Row id accessor for stable row identity                                                                                                                                                                                                                                                                                                                                                                                                                            |
| aria-label               | `string \| undefined`                                  |         | Accessible label for the table                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| sorting                  | `TableSortingState \| undefined`                       |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| onSortingChange          | `complex`                                              |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| manualSorting            | `boolean \| undefined`                                 |         | When `true`, disables TanStack's client-side sort. The table renders rows in the order they appear in `data` and only fires `onSortingChange` on header clicks. Use this when sorting is performed server-side and the server returns pre-sorted rows. Default: `false` (client-side sort via TanStack's `getSortedRowModel`).                                                                                                                                     |
| rowSelection             | `TableRowSelectionState \| undefined`                  |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| onRowSelectionChange     | `complex`                                              |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| enableSelectAllRows      | `boolean \| undefined`                                 |         | Renders the master checkbox in the selection column header. Set `false` when "all rows" is not a selection the consumer can act on — a listing whose scope the consumer cannot express, for instance — leaving per-row checkboxes (shift-click range selection included) as the only way to select. The column itself stays, so the layout does not shift. Default: `true`.                                                                                        |
| onSelectAllRows          | `((selected: boolean) => void) \| undefined`           |         | Called when the master checkbox is toggled, with its new state, in addition to the `onRowSelectionChange` the toggle produces. The state change alone cannot identify the gesture: "every loaded row is selected" is also what checking the last row by hand produces. A consumer that treats the master checkbox as meaning more than the loaded rows — everything matching a server-side filter, say — needs to know the difference, which is what this reports. |
| columnSizing             | `TableColumnSizingState \| undefined`                  |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| onColumnSizingChange     | `complex`                                              |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| columnPinning            | `TableColumnPinningState \| undefined`                 |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| onColumnPinningChange    | `complex`                                              |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| columnOrder              | `string[] \| undefined`                                |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| onColumnOrderChange      | `TableOnChangeFn<string[]> \| undefined`               |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| grouping                 | `TableGroupingState \| undefined`                      |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| onGroupingChange         | `complex`                                              |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| renderGroupRow           | `((row: TableRow<T>) => ReactNode) \| undefined`       |         | Group row content for `getSubRows` grouping. Passing it switches the table to sub-row grouping mode: the auto-injected expand column is dropped, so the content is responsible for its own expand/collapse toggle (`row.getToggleExpandedHandler()`). Rendered only with `groupRowSpan: 'full'`. With the default `'master'` the group row shows its master (first data) column cell, as before.                                                                   |
| groupRowSpan             | `"full" \| "master" \| undefined`                      |         | How a group row lays out its content. - `'master'` (default): the master column's cell, then empty filler cells — the content is confined to the master column width. - `'full'`: `renderGroupRow(row)` in a single cell spanning every data column (system cells — selection, drag handle — keep their own cells). Use for a group header with a title on the left and actions pinned to the right edge of the table. Requires `renderGroupRow`.                  |
| groupExpandColumn        | `boolean \| undefined`                                 |         | Keep the expand column in sub-row grouping mode (`renderGroupRow`), which otherwise drops it and leaves the toggle to the group row's content. The column comes first, ahead of the drag handle, and shows a toggle on group rows only — so the rows of a group, drag handle included, sit inside it.                                                                                                                                                              |
| getSubRows               | `((row: T) => T[] \| undefined) \| undefined`          |         | Sub-row accessor for hierarchical/tree data (used with renderGroupRow)                                                                                                                                                                                                                                                                                                                                                                                             |
| showEmptyGroups          | `boolean \| undefined`                                 |         | Render a top-level row whose `getSubRows` returns an empty array as an (empty) group row instead of a plain row — for groups that stay visible with nothing in them yet. Return `undefined` for rows that are not groups.                                                                                                                                                                                                                                          |
| expanded                 | `TableExpandedState \| undefined`                      |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| onExpandedChange         | `complex`                                              |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| renderExpandedRow        | `((row: TableRow<T>) => ReactNode) \| undefined`       |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| columnVisibility         | `TableVisibilityState \| undefined`                    |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| onColumnVisibilityChange | `complex`                                              |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| defaultColumnVisibility  | `TableVisibilityState \| undefined`                    |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| defaultColumnOrder       | `string[] \| undefined`                                |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| columnGroups             | `TableColumnGroup[] \| undefined`                      |         | Labeled sections for the column-settings menu. When set, the menu renders these groups (flat, non-reorderable — like the FilterInput field menu) instead of the default pinned/unpinned drag-reorder list. See \{@link TableColumnGroup}.                                                                                                                                                                                                                          |
| onSettingsOpenChange     | `((open: boolean) => void) \| undefined`               |         | Fired when the built-in column-settings menu opens (`true`) or closes (`false`). The menu's open state otherwise lives inside the DS and is not observable, so consumers that want to defer expensive work — e.g. refetch once with the final column selection instead of on every toggle — can hook the close edge here.                                                                                                                                          |
| deferColumnVisibility    | `boolean \| undefined`                                 |         | Hold column-visibility toggles in the built-in settings menu until it closes, then apply them as one `columnVisibility` change. Without it each switch shows or hides its column at once — a column whose data the consumer only fetches on close then sits empty while the menu is open.                                                                                                                                                                          |
| virtualized              | `TableVirtualized \| undefined`                        |         | Enable row virtualization. `'container'` virtualizes within the table's own scroll container; `'window'` follows the page scroll — the nearest scrollable ancestor, or the browser window.                                                                                                                                                                                                                                                                         |
| stretch                  | `boolean \| undefined`                                 |         | Whether columns stretch proportionally to fill the container width. When `false`, columns use their defined sizes and the table may be narrower than its container. Default: `true`.                                                                                                                                                                                                                                                                               |
| estimateRowHeight        | `((index: number) => number) \| undefined`             |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| overscan                 | `number \| undefined`                                  |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| onEndReached             | `(() => void) \| undefined`                            |         | Callback fired when the user scrolls near the end (bottom) of the table                                                                                                                                                                                                                                                                                                                                                                                            |
| onEndReachedThreshold    | `number \| undefined`                                  |         | Distance from the bottom (in px) to trigger onEndReached (default: 200)                                                                                                                                                                                                                                                                                                                                                                                            |
| onStartReached           | `(() => void) \| undefined`                            |         | Callback fired when the user scrolls near the start (top) of the table                                                                                                                                                                                                                                                                                                                                                                                             |
| onStartReachedThreshold  | `number \| undefined`                                  |         | Distance from the top (in px) to trigger onStartReached (default: 200)                                                                                                                                                                                                                                                                                                                                                                                             |
| initialScrollToRowId     | `string \| undefined`                                  |         | Row id to anchor the initial scroll position to. The table scrolls this row into view on mount and arms the edge detectors only after that initial scroll settles. Use for deep-linking into the middle of a dataset.                                                                                                                                                                                                                                              |
| onRowReorder             | `((event: TableRowReorderEvent) => void) \| undefined` |         | Callback fired when a row is dragged to a new position. When provided, a drag-handle column is auto-injected and row DnD is enabled. With `getSubRows` grouping, group rows stay put and a row moves only among the rows of its own group.                                                                                                                                                                                                                         |
| getRowCanDrag            | `((row: TableRow<T>) => boolean) \| undefined`         |         | Per-row drag control for `onRowReorder`. A row for which it returns `false` shows a disabled grip (none on a full-width row), cannot be dragged and is not a drop target — the other rows of its group move around it. Group rows never move regardless. Default: every non-group row can be dragged.                                                                                                                                                              |
| renderFullWidthRow       | `((row: TableRow<T>) => ReactNode) \| undefined`       |         | Renders a non-group row as a single cell spanning every data column — a banner or notice row among regular rows. Return `undefined` to render the row normally, with a cell per column. System cells (selection, drag handle) keep their own cells. Pair with `getRowCanDrag` to keep such a row in place in a reorderable table. Called on every render of the row: return elements, do not call hooks in the callback itself (components it returns may).        |
| onMasterCellClick        | `((rowId: string) => void) \| undefined`               |         | Callback fired when the master cell is clicked. Receives the row ID.                                                                                                                                                                                                                                                                                                                                                                                               |
| activeRowId              | `string \| null \| undefined`                          |         | ID of the currently active (highlighted) row, or null. Controls row highlighting via data-preview-active attribute.                                                                                                                                                                                                                                                                                                                                                |
| data-testid              | `string \| undefined`                                  |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |

### BulkBarSummaryClear

| Prop           | Type                                                 | Default | Description |
| -------------- | ---------------------------------------------------- | ------- | ----------- |
| title          | `string \| undefined`                                |         |             |
| defaultChecked | `boolean \| undefined`                               |         |             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |             |
| autoFocus      | `boolean \| undefined`                               |         |             |
| id             | `string \| undefined`                                |         |             |
| tabIndex       | `number \| undefined`                                |         |             |
| rel            | `string \| undefined`                                |         |             |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |             |
| disabled       | `boolean \| undefined`                               | `false` |             |
| name           | `string \| undefined`                                |         |             |
| value          | `string \| number \| readonly string[] \| undefined` |         |             |
| data-testid    | `string \| undefined`                                |         |             |

### BulkBarSummaryCount

| Prop           | Type                                                                                                                                               | Default       | Description                                    |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------- |
| count          | `number \| undefined`                                                                                                                              |               | Renders the default "\{count} selected" label. |
| title          | `string \| undefined`                                                                                                                              |               |                                                |
| defaultChecked | `boolean \| undefined`                                                                                                                             |               |                                                |
| defaultValue   | `string \| number \| readonly string[] \| undefined`                                                                                               |               |                                                |
| autoFocus      | `boolean \| undefined`                                                                                                                             |               |                                                |
| id             | `string \| undefined`                                                                                                                              |               |                                                |
| tabIndex       | `number \| undefined`                                                                                                                              |               |                                                |
| rel            | `string \| undefined`                                                                                                                              |               |                                                |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`                                                                                                      |               |                                                |
| color          | `{ primary: string; 'primary-alt': string; secondary: string; 'secondary-alt': string; 'tertiary-alt': string; danger: string; inherit: string; }` | `primary-alt` |                                                |
| size           | `{ xs: string; sm: string; md: string; lg: string; xl: string; }`                                                                                  | `sm`          |                                                |
| weight         | `{ light: string; regular: string; medium: string; bold: string; }`                                                                                | `medium`      |                                                |
| data-testid    | `string \| undefined`                                                                                                                              |               |                                                |

### BulkBarSummary

| Prop           | Type                                                 | Default | Description |
| -------------- | ---------------------------------------------------- | ------- | ----------- |
| title          | `string \| undefined`                                |         |             |
| defaultChecked | `boolean \| undefined`                               |         |             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |             |
| autoFocus      | `boolean \| undefined`                               |         |             |
| id             | `string \| undefined`                                |         |             |
| tabIndex       | `number \| undefined`                                |         |             |
| rel            | `string \| undefined`                                |         |             |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |             |
| data-testid    | `string \| undefined`                                |         |             |

### BulkBarSummarySelectAll

| Prop           | Type                                                 | Default | Description |
| -------------- | ---------------------------------------------------- | ------- | ----------- |
| title          | `string \| undefined`                                |         |             |
| defaultChecked | `boolean \| undefined`                               |         |             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |             |
| autoFocus      | `boolean \| undefined`                               |         |             |
| id             | `string \| undefined`                                |         |             |
| tabIndex       | `number \| undefined`                                |         |             |
| rel            | `string \| undefined`                                |         |             |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |             |
| disabled       | `boolean \| undefined`                               | `false` |             |
| name           | `string \| undefined`                                |         |             |
| value          | `string \| number \| readonly string[] \| undefined` |         |             |
| data-testid    | `string \| undefined`                                |         |             |

### EditableSelectCell

| Prop           | Type                                                 | Default  | Description                                                     |
| -------------- | ---------------------------------------------------- | -------- | --------------------------------------------------------------- |
| value \*       | `string \| null`                                     |          | Current committed value; `null` / `''` renders the placeholder. |
| onCommit \*    | `(value: string) => void`                            |          | Called with the picked value when the selection changes.        |
| items \*       | `SelectDataItem[]`                                   |          | Options for the dropdown.                                       |
| placeholder    | `ReactNode`                                          | `Select` | Shown when nothing is selected. Defaults to `'Select'`.         |
| title          | `string \| undefined`                                |          |                                                                 |
| defaultChecked | `boolean \| undefined`                               |          |                                                                 |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |          |                                                                 |
| autoFocus      | `boolean \| undefined`                               |          |                                                                 |
| id             | `string \| undefined`                                |          |                                                                 |
| tabIndex       | `number \| undefined`                                |          |                                                                 |
| rel            | `string \| undefined`                                |          |                                                                 |
| data-testid    | `string \| undefined`                                |          |                                                                 |

### EditableTextCell

| Prop           | Type                                                 | Default | Description                                                                |
| -------------- | ---------------------------------------------------- | ------- | -------------------------------------------------------------------------- |
| value \*       | `string`                                             |         | Current committed value.                                                   |
| onCommit \*    | `(value: string) => void`                            |         | Called with the new value on Enter / blur (only when it actually changed). |
| title          | `string \| undefined`                                |         |                                                                            |
| defaultChecked | `boolean \| undefined`                               |         |                                                                            |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |                                                                            |
| autoFocus      | `boolean \| undefined`                               |         |                                                                            |
| id             | `string \| undefined`                                |         |                                                                            |
| tabIndex       | `number \| undefined`                                |         |                                                                            |
| rel            | `string \| undefined`                                |         |                                                                            |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |                                                                            |
| data-testid    | `string \| undefined`                                |         |                                                                            |

### TableColumnMenuHideItem

| Prop           | Type                                                       | Default | Description                                                       |
| -------------- | ---------------------------------------------------------- | ------- | ----------------------------------------------------------------- |
| inset          | `boolean \| undefined`                                     |         |                                                                   |
| disabled       | `boolean \| undefined`                                     |         |                                                                   |
| onSelect       | `(() => void) \| undefined`                                |         |                                                                   |
| value          | `string \| undefined`                                      |         | Explicit value for programmatic highlighting via highlightedValue |
| title          | `string \| undefined`                                      |         |                                                                   |
| defaultChecked | `boolean \| undefined`                                     |         |                                                                   |
| defaultValue   | `string \| number \| readonly string[] \| undefined`       |         |                                                                   |
| autoFocus      | `boolean \| undefined`                                     |         |                                                                   |
| tabIndex       | `number \| undefined`                                      |         |                                                                   |
| rel            | `string \| undefined`                                      |         |                                                                   |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`              |         |                                                                   |
| variant        | `{ default: string; brand: string; destructive: string; }` |         |                                                                   |
| data-testid    | `string \| undefined`                                      |         |                                                                   |

### TableColumnMenuMoveLeftItem

| Prop           | Type                                                       | Default | Description                                                       |
| -------------- | ---------------------------------------------------------- | ------- | ----------------------------------------------------------------- |
| inset          | `boolean \| undefined`                                     |         |                                                                   |
| disabled       | `boolean \| undefined`                                     |         |                                                                   |
| onSelect       | `(() => void) \| undefined`                                |         |                                                                   |
| value          | `string \| undefined`                                      |         | Explicit value for programmatic highlighting via highlightedValue |
| title          | `string \| undefined`                                      |         |                                                                   |
| defaultChecked | `boolean \| undefined`                                     |         |                                                                   |
| defaultValue   | `string \| number \| readonly string[] \| undefined`       |         |                                                                   |
| autoFocus      | `boolean \| undefined`                                     |         |                                                                   |
| tabIndex       | `number \| undefined`                                      |         |                                                                   |
| rel            | `string \| undefined`                                      |         |                                                                   |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`              |         |                                                                   |
| variant        | `{ default: string; brand: string; destructive: string; }` |         |                                                                   |
| data-testid    | `string \| undefined`                                      |         |                                                                   |

### TableColumnMenuMoveRightItem

| Prop           | Type                                                       | Default | Description                                                       |
| -------------- | ---------------------------------------------------------- | ------- | ----------------------------------------------------------------- |
| inset          | `boolean \| undefined`                                     |         |                                                                   |
| disabled       | `boolean \| undefined`                                     |         |                                                                   |
| onSelect       | `(() => void) \| undefined`                                |         |                                                                   |
| value          | `string \| undefined`                                      |         | Explicit value for programmatic highlighting via highlightedValue |
| title          | `string \| undefined`                                      |         |                                                                   |
| defaultChecked | `boolean \| undefined`                                     |         |                                                                   |
| defaultValue   | `string \| number \| readonly string[] \| undefined`       |         |                                                                   |
| autoFocus      | `boolean \| undefined`                                     |         |                                                                   |
| tabIndex       | `number \| undefined`                                      |         |                                                                   |
| rel            | `string \| undefined`                                      |         |                                                                   |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`              |         |                                                                   |
| variant        | `{ default: string; brand: string; destructive: string; }` |         |                                                                   |
| data-testid    | `string \| undefined`                                      |         |                                                                   |

### TableColumnMenuPinItem

| Prop           | Type                                                       | Default | Description                                                       |
| -------------- | ---------------------------------------------------------- | ------- | ----------------------------------------------------------------- |
| inset          | `boolean \| undefined`                                     |         |                                                                   |
| disabled       | `boolean \| undefined`                                     |         |                                                                   |
| onSelect       | `(() => void) \| undefined`                                |         |                                                                   |
| value          | `string \| undefined`                                      |         | Explicit value for programmatic highlighting via highlightedValue |
| title          | `string \| undefined`                                      |         |                                                                   |
| defaultChecked | `boolean \| undefined`                                     |         |                                                                   |
| defaultValue   | `string \| number \| readonly string[] \| undefined`       |         |                                                                   |
| autoFocus      | `boolean \| undefined`                                     |         |                                                                   |
| tabIndex       | `number \| undefined`                                      |         |                                                                   |
| rel            | `string \| undefined`                                      |         |                                                                   |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`              |         |                                                                   |
| variant        | `{ default: string; brand: string; destructive: string; }` |         |                                                                   |
| data-testid    | `string \| undefined`                                      |         |                                                                   |

### TableColumnMenu

| Prop           | Type                                                 | Default | Description                            |
| -------------- | ---------------------------------------------------- | ------- | -------------------------------------- |
| column \*      | `complex`                                            |         | Column from the header render context. |
| title          | `string \| undefined`                                |         |                                        |
| defaultChecked | `boolean \| undefined`                               |         |                                        |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |                                        |
| autoFocus      | `boolean \| undefined`                               |         |                                        |
| id             | `string \| undefined`                                |         |                                        |
| tabIndex       | `number \| undefined`                                |         |                                        |
| rel            | `string \| undefined`                                |         |                                        |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |                                        |
| disabled       | `boolean \| undefined`                               |         |                                        |
| name           | `string \| undefined`                                |         |                                        |
| value          | `string \| number \| readonly string[] \| undefined` |         |                                        |
| data-testid    | `string \| undefined`                                |         |                                        |

### TableColumnMenuSortItem

| Prop           | Type                                                       | Default | Description                                                       |
| -------------- | ---------------------------------------------------------- | ------- | ----------------------------------------------------------------- |
| direction \*   | `"desc" \| "asc"`                                          |         | Direction this item commits when selected.                        |
| inset          | `boolean \| undefined`                                     |         |                                                                   |
| disabled       | `boolean \| undefined`                                     |         |                                                                   |
| onSelect       | `(() => void) \| undefined`                                |         |                                                                   |
| value          | `string \| undefined`                                      |         | Explicit value for programmatic highlighting via highlightedValue |
| title          | `string \| undefined`                                      |         |                                                                   |
| defaultChecked | `boolean \| undefined`                                     |         |                                                                   |
| defaultValue   | `string \| number \| readonly string[] \| undefined`       |         |                                                                   |
| autoFocus      | `boolean \| undefined`                                     |         |                                                                   |
| tabIndex       | `number \| undefined`                                      |         |                                                                   |
| rel            | `string \| undefined`                                      |         |                                                                   |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`              |         |                                                                   |
| variant        | `{ default: string; brand: string; destructive: string; }` |         |                                                                   |
| data-testid    | `string \| undefined`                                      |         |                                                                   |

### TableScrollHandlerLeft

| Prop           | Type                                                 | Default | Description |
| -------------- | ---------------------------------------------------- | ------- | ----------- |
| title          | `string \| undefined`                                |         |             |
| defaultChecked | `boolean \| undefined`                               |         |             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |             |
| autoFocus      | `boolean \| undefined`                               |         |             |
| id             | `string \| undefined`                                |         |             |
| tabIndex       | `number \| undefined`                                |         |             |
| rel            | `string \| undefined`                                |         |             |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |             |
| disabled       | `boolean \| undefined`                               |         |             |
| name           | `string \| undefined`                                |         |             |
| value          | `string \| number \| readonly string[] \| undefined` |         |             |

### TableScrollHandlerRight

| Prop           | Type                                                 | Default | Description |
| -------------- | ---------------------------------------------------- | ------- | ----------- |
| title          | `string \| undefined`                                |         |             |
| defaultChecked | `boolean \| undefined`                               |         |             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |             |
| autoFocus      | `boolean \| undefined`                               |         |             |
| id             | `string \| undefined`                                |         |             |
| tabIndex       | `number \| undefined`                                |         |             |
| rel            | `string \| undefined`                                |         |             |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |             |
| disabled       | `boolean \| undefined`                               |         |             |
| name           | `string \| undefined`                                |         |             |
| value          | `string \| number \| readonly string[] \| undefined` |         |             |

### TableSettingsMenuItem

| Prop           | Type                                                 | Default | Description                                                                                         |
| -------------- | ---------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------- |
| columnId \*    | `string`                                             |         | Column id from the table's column definitions.                                                      |
| label          | `string \| undefined`                                |         | Specifies the localized strings that identifies the accessibility elements and their states         |
| title          | `string \| undefined`                                |         |                                                                                                     |
| defaultChecked | `boolean \| undefined`                               |         |                                                                                                     |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |                                                                                                     |
| autoFocus      | `boolean \| undefined`                               |         |                                                                                                     |
| id             | `string \| undefined`                                |         |                                                                                                     |
| tabIndex       | `number \| undefined`                                |         |                                                                                                     |
| rel            | `string \| undefined`                                |         |                                                                                                     |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |                                                                                                     |
| name           | `string \| undefined`                                |         | The name of the input field in a switch (Useful for form submission).                               |
| value          | `string \| number \| undefined`                      |         | The value of switch input. Useful for form submission.                                              |
| asChild        | `boolean \| undefined`                               |         | Use the provided child element as the default rendered element, combining their props and behavior. |
| data-testid    | `string \| undefined`                                |         |                                                                                                     |
| ids            | `ElementIds \| undefined`                            |         | The ids of the elements in the switch. Useful for composition.                                      |
| readOnly       | `boolean \| undefined`                               |         | Whether the switch is read-only                                                                     |
| required       | `boolean \| undefined`                               |         | If `true`, the switch input is marked as required,                                                  |
| htmlFor        | `string \| undefined`                                |         |                                                                                                     |
| invalid        | `boolean \| undefined`                               |         | If `true`, the switch is marked as invalid.                                                         |
| a11yMode       | `boolean \| undefined`                               |         |                                                                                                     |

### TableSettingsMenu

| Prop           | Type                                                 | Default | Description |
| -------------- | ---------------------------------------------------- | ------- | ----------- |
| title          | `string \| undefined`                                |         |             |
| defaultChecked | `boolean \| undefined`                               |         |             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |             |
| autoFocus      | `boolean \| undefined`                               |         |             |
| id             | `string \| undefined`                                |         |             |
| tabIndex       | `number \| undefined`                                |         |             |
| rel            | `string \| undefined`                                |         |             |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |             |
| disabled       | `boolean \| undefined`                               |         |             |
| name           | `string \| undefined`                                |         |             |
| value          | `string \| number \| readonly string[] \| undefined` |         |             |
| data-testid    | `string \| undefined`                                |         |             |

### TableSettingsMenuReset

| Prop           | Type                                                 | Default | Description |
| -------------- | ---------------------------------------------------- | ------- | ----------- |
| title          | `string \| undefined`                                |         |             |
| defaultChecked | `boolean \| undefined`                               |         |             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |             |
| autoFocus      | `boolean \| undefined`                               |         |             |
| id             | `string \| undefined`                                |         |             |
| tabIndex       | `number \| undefined`                                |         |             |
| rel            | `string \| undefined`                                |         |             |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |             |
| disabled       | `boolean \| undefined`                               |         |             |
| name           | `string \| undefined`                                |         |             |
| value          | `string \| number \| readonly string[] \| undefined` |         |             |
| data-testid    | `string \| undefined`                                |         |             |

### TableSettingsMenuSearch

| Prop           | Type                                                 | Default  | Description |
| -------------- | ---------------------------------------------------- | -------- | ----------- |
| title          | `string \| undefined`                                |          |             |
| pattern        | `string \| undefined`                                |          |             |
| defaultChecked | `boolean \| undefined`                               |          |             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |          |             |
| autoFocus      | `boolean \| undefined`                               |          |             |
| id             | `string \| undefined`                                |          |             |
| tabIndex       | `number \| undefined`                                |          |             |
| rel            | `string \| undefined`                                |          |             |
| disabled       | `boolean \| undefined`                               |          |             |
| name           | `string \| undefined`                                |          |             |
| alt            | `string \| undefined`                                |          |             |
| height         | `string \| number \| undefined`                      |          |             |
| src            | `string \| undefined`                                |          |             |
| width          | `string \| number \| undefined`                      |          |             |
| autoComplete   | `HTMLInputAutoCompleteAttribute \| undefined`        |          |             |
| maxLength      | `number \| undefined`                                |          |             |
| minLength      | `number \| undefined`                                |          |             |
| placeholder    | `string \| undefined`                                | `Search` |             |
| readOnly       | `boolean \| undefined`                               |          |             |
| required       | `boolean \| undefined`                               |          |             |
| checked        | `boolean \| undefined`                               |          |             |
| step           | `string \| number \| undefined`                      |          |             |
| accept         | `string \| undefined`                                |          |             |
| max            | `string \| number \| undefined`                      |          |             |
| min            | `string \| number \| undefined`                      |          |             |
| multiple       | `boolean \| undefined`                               |          |             |
| data-testid    | `string \| undefined`                                |          |             |

### TableSortTrigger

| Prop           | Type                                                 | Default | Description                                                                                                                                                                                                                                           |
| -------------- | ---------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| column \*      | `complex`                                            |         | Column from the header render context — wires the trigger to TanStack's sort state. Pass `header.column` directly: `tsx header: ({ column }) => ( <TableSortTrigger column={column} data-analytics-id='users.sort.name'> Name </TableSortTrigger> ) ` |
| title          | `string \| undefined`                                |         |                                                                                                                                                                                                                                                       |
| defaultChecked | `boolean \| undefined`                               |         |                                                                                                                                                                                                                                                       |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |                                                                                                                                                                                                                                                       |
| autoFocus      | `boolean \| undefined`                               |         |                                                                                                                                                                                                                                                       |
| id             | `string \| undefined`                                |         |                                                                                                                                                                                                                                                       |
| tabIndex       | `number \| undefined`                                |         |                                                                                                                                                                                                                                                       |
| rel            | `string \| undefined`                                |         |                                                                                                                                                                                                                                                       |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |                                                                                                                                                                                                                                                       |
| disabled       | `boolean \| undefined`                               |         |                                                                                                                                                                                                                                                       |
| name           | `string \| undefined`                                |         |                                                                                                                                                                                                                                                       |
| value          | `string \| number \| readonly string[] \| undefined` |         |                                                                                                                                                                                                                                                       |
| data-testid    | `string \| undefined`                                |         |                                                                                                                                                                                                                                                       |
