> ## Documentation Index
> Fetch the complete documentation index at: https://neo.tvk.company/llms.txt
> Use this file to discover all available pages before exploring further.

# Table

> A data table widget with sorting, row/column reordering, selection, column hiding, and efficient scrolling for large datasets.

## Examples

<Tabs>
  <Tab title="Basic Usage">
    <Frame caption="Simple data table with column headers.">
      <img src="https://mintcdn.com/tvk/M1ZGdn8TY0ukkJe8/images/widgets/utilities/table/table_basic_light.png?fit=max&auto=format&n=M1ZGdn8TY0ukkJe8&q=85&s=bb9da3485a43bcf1f4c37efd9478d546" noZoom className="block dark:hidden" width="1536" height="384" data-path="images/widgets/utilities/table/table_basic_light.png" />

      <img src="https://mintcdn.com/tvk/M1ZGdn8TY0ukkJe8/images/widgets/utilities/table/table_basic_dark.png?fit=max&auto=format&n=M1ZGdn8TY0ukkJe8&q=85&s=3df78ffc0b3ae331b884fd282cf8c692" noZoom className="hidden dark:block" width="1536" height="384" data-path="images/widgets/utilities/table/table_basic_dark.png" />
    </Frame>

    <CodeGroup>
      ```dart Basic Table lines theme={null}
      NeoTable(
        columns: [
          NeoTableColumn(id: "name", label: "Name", flex: 2),
          NeoTableColumn(id: "email", label: "Email", flex: 3),
          NeoTableColumn(id: "role", label: "Role", flex: 1),
        ],
        rows: [
          NeoTableRow(
            id: "user1",
            cells: {
              "name": Text("John Doe"),
              "email": Text("john@example.com"),
              "role": Text("Admin"),
            },
          ),
          NeoTableRow(
            id: "user2",
            cells: {
              "name": Text("Jane Smith"),
              "email": Text("jane@example.com"),
              "role": Text("Editor"),
            },
          ),
        ],
      ),
      ```
    </CodeGroup>
  </Tab>

  <Tab title="With Sorting">
    <Frame caption="Table with sortable columns and sort state indicators.">
      <img src="https://mintcdn.com/tvk/M1ZGdn8TY0ukkJe8/images/widgets/utilities/table/table_sorting_light.png?fit=max&auto=format&n=M1ZGdn8TY0ukkJe8&q=85&s=119a25522615f070f24490cb8ba1df6a" noZoom className="block dark:hidden" width="1536" height="384" data-path="images/widgets/utilities/table/table_sorting_light.png" />

      <img src="https://mintcdn.com/tvk/M1ZGdn8TY0ukkJe8/images/widgets/utilities/table/table_sorting_dark.png?fit=max&auto=format&n=M1ZGdn8TY0ukkJe8&q=85&s=f882a372cc4269953577d5817402552e" noZoom className="hidden dark:block" width="1536" height="384" data-path="images/widgets/utilities/table/table_sorting_dark.png" />
    </Frame>

    <CodeGroup>
      ```dart Sortable Table lines theme={null}
      final sortState = useState<NeoTableSortState?>(
        const NeoTableSortState(columnId: "name", direction: .ascending),
      );

      NeoTable(
        sortState: sortState.value,
        onSortChanged: (state) => sortState.value = state,
        columns: [
          NeoTableColumn(id: "name", label: "Name", flex: 2, isSortable: true),
          NeoTableColumn(id: "email", label: "Email", flex: 3, isSortable: true),
          NeoTableColumn(id: "role", label: "Role", flex: 1),
        ],
        rows: sortedRows,
      ),
      ```
    </CodeGroup>
  </Tab>

  <Tab title="With Selection">
    <Frame caption="Table with row selection and select-all.">
      <img src="https://mintcdn.com/tvk/M1ZGdn8TY0ukkJe8/images/widgets/utilities/table/table_selection_light.png?fit=max&auto=format&n=M1ZGdn8TY0ukkJe8&q=85&s=e57d002c412618aa693fc586dc48960a" noZoom className="block dark:hidden" width="1536" height="384" data-path="images/widgets/utilities/table/table_selection_light.png" />

      <img src="https://mintcdn.com/tvk/M1ZGdn8TY0ukkJe8/images/widgets/utilities/table/table_selection_dark.png?fit=max&auto=format&n=M1ZGdn8TY0ukkJe8&q=85&s=b5fb8b4e5830c7e47e83cf9818f5ead1" noZoom className="hidden dark:block" width="1536" height="384" data-path="images/widgets/utilities/table/table_selection_dark.png" />
    </Frame>

    <CodeGroup>
      ```dart Selectable Table lines theme={null}
      NeoTable(
        isSelectable: true,
        selectedRowIds: selectedRows.value,
        onSelectionChanged: (ids) => selectedRows.value = ids,
        columns: [
          NeoTableColumn(id: "name", label: "Name", flex: 2),
          NeoTableColumn(id: "email", label: "Email", flex: 3),
          NeoTableColumn(id: "role", label: "Role", flex: 1),
        ],
        rows: [
          NeoTableRow(
            id: "user1",
            cells: {
              "name": Text("John Doe"),
              "email": Text("john@example.com"),
              "role": Text("Admin"),
            },
          ),
        ],
      ),
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Row Reordering">
    <Frame caption="Table with drag-to-reorder rows.">
      <img src="https://mintlify.s3.us-west-1.amazonaws.com/tvk/images/widgets/utilities/table/table_reorder_light.png" noZoom className="block dark:hidden" />

      <img src="https://mintlify.s3.us-west-1.amazonaws.com/tvk/images/widgets/utilities/table/table_reorder_dark.png" noZoom className="hidden dark:block" />
    </Frame>

    <CodeGroup>
      ```dart Row Reordering lines theme={null}
      NeoTable(
        isRowReorderable: true,
        onRowReorder: (rowId, oldIndex, newIndex) {
          final updated = List.of(rows.value);
          updated.insert(newIndex, updated.removeAt(oldIndex));
          rows.value = updated;
        },
        columns: [
          NeoTableColumn(id: "name", label: "Name", flex: 2),
          NeoTableColumn(id: "priority", label: "Priority", flex: 1),
        ],
        rows: rows.value,
      ),
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Border and Dividers">
    <Frame caption="Table with border and row/column dividers.">
      <img src="https://mintcdn.com/tvk/M1ZGdn8TY0ukkJe8/images/widgets/utilities/table/table_border_light.png?fit=max&auto=format&n=M1ZGdn8TY0ukkJe8&q=85&s=66e6b775cdbc90757995edfe81c0484d" noZoom className="block dark:hidden" width="1536" height="384" data-path="images/widgets/utilities/table/table_border_light.png" />

      <img src="https://mintcdn.com/tvk/M1ZGdn8TY0ukkJe8/images/widgets/utilities/table/table_border_dark.png?fit=max&auto=format&n=M1ZGdn8TY0ukkJe8&q=85&s=b620ccfab8f7b3da6a06caa0e1af9ee0" noZoom className="hidden dark:block" width="1536" height="384" data-path="images/widgets/utilities/table/table_border_dark.png" />
    </Frame>

    <CodeGroup>
      ```dart Border and Dividers lines theme={null}
      NeoTable(
        showBorder: true,
        showRowDividers: true,
        showColumnDividers: true,
        columns: [
          NeoTableColumn(id: "product", label: "Product", flex: 3),
          NeoTableColumn(id: "price", label: "Price", flex: 1),
          NeoTableColumn(id: "stock", label: "Stock", flex: 1),
        ],
        rows: products.map((p) => NeoTableRow(
          id: p.id,
          cells: {
            "product": Text(p.name),
            "price": Text("€${p.price}"),
            "stock": Text("${p.stock}"),
          },
        )).toList(),
      ),
      ```
    </CodeGroup>
  </Tab>
</Tabs>

## Properties

### Required

<ParamField path="columns" type="List<NeoTableColumn>" required>
  The column definitions that determine the table's structure and headers.
</ParamField>

<ParamField path="rows" type="List<NeoTableRow>" required>
  The data rows to display. Each row's `cells` map must include a widget for every visible column ID.
</ParamField>

### Content

<ParamField path="onRowTap" type="ValueChanged<String>">
  Called when a row is tapped. Receives the row ID.
</ParamField>

<ParamField path="onRowHover" type="ValueChanged<({String rowId, bool isHovered})>">
  Called when a row's hover state changes. Receives the row ID and hover status.
</ParamField>

<ParamField path="activeRowId" type="String">
  The ID of the currently active row. The active row receives distinct highlight styling.
</ParamField>

<ParamField path="emptyStateBuilder" type="Widget">
  Widget to display when `rows` is empty. Falls back to an empty table body if not provided.
</ParamField>

### Layout

<ParamField path="fixedRowHeight" type="double" default="48">
  Fixed height for all rows in logical pixels.
</ParamField>

<ParamField path="headerHeight" type="double" default="40">
  Height of the sticky column-header row in logical pixels.
</ParamField>

<ParamField path="showBorder" type="bool" default="false">
  Whether to show a border around the entire table.
</ParamField>

<ParamField path="showRowDividers" type="bool" default="false">
  Whether to show horizontal divider lines between rows.
</ParamField>

<ParamField path="showColumnDividers" type="bool" default="false">
  Whether to show vertical divider lines between columns.
</ParamField>

<ParamField path="scrollController" type="ScrollController">
  Scroll controller for the table body. Required when using `NeoTable.scrollToRow`. An internal controller is used when not provided.
</ParamField>

<ParamField path="hiddenColumnIds" type="Set<String>">
  Set of column IDs to hide. The columns remain in `columns` so their cells are still defined in rows — they are simply not rendered.
</ParamField>

### Sorting

<ParamField path="sortState" type="NeoTableSortState">
  The current sort state (column ID + direction). `null` means unsorted.
</ParamField>

<ParamField path="defaultSortState" type="NeoTableSortState">
  The sort state to fall back to when no explicit `sortState` is set. The default-sort column cannot be fully unsorted — it cycles between ascending and descending.
</ParamField>

<ParamField path="onSortChanged" type="ValueChanged<NeoTableSortState?>">
  Called when the user clicks a sortable column header. Receives the new `NeoTableSortState`, or `null` when returning to the unsorted state.
</ParamField>

### Selection

<ParamField path="isSelectable" type="bool" default="false">
  Whether rows can be selected with checkboxes. When `true`, `onSelectionChanged` must be provided.
</ParamField>

<ParamField path="selectedRowIds" type="Set<String>">
  The currently selected row IDs.
</ParamField>

<ParamField path="onSelectionChanged" type="ValueChanged<Set<String>>">
  Called when the selection changes. Required when `isSelectable` is `true`.
</ParamField>

### Reordering

<ParamField path="isRowReorderable" type="bool" default="false">
  Whether rows can be reordered by dragging. When `true`, `onRowReorder` must be provided.
</ParamField>

<ParamField path="onRowReorder" type="Function(String rowId, int oldIndex, int newIndex)">
  Called when the user drops a row into a new position. Required when `isRowReorderable` is `true`.
</ParamField>

<ParamField path="isColumnReorderable" type="bool" default="false">
  Whether columns can be reordered by dragging their headers. When `true`, `onColumnReorder` must be provided.
</ParamField>

<ParamField path="onColumnReorder" type="Function(String columnId, int oldIndex, int newIndex)">
  Called when the user drops a column header into a new position. Required when `isColumnReorderable` is `true`.
</ParamField>

### State

<ParamField path="isLoading" type="bool" default="false">
  Displays a loading indicator over the table body and disables interaction.
</ParamField>

## Static Helpers

### NeoTable.scrollToRow()

Animates the table body scroll so the given row is aligned to the top of the viewport (below the fixed header).

```dart theme={null}
NeoTable.scrollToRow(
  controller: scrollController,
  rows: rows,
  rowId: "user5",
  fixedRowHeight: 48,
);
```

<ParamField path="controller" type="ScrollController" required>
  The same `ScrollController` passed to the table's `scrollController` prop.
</ParamField>

<ParamField path="rows" type="List<NeoTableRow>" required>
  The current row list — must match the table's `rows` prop.
</ParamField>

<ParamField path="rowId" type="String" required>
  The ID of the row to scroll to.
</ParamField>

<ParamField path="fixedRowHeight" type="double" required>
  Must match the table's `fixedRowHeight` prop.
</ParamField>

<ParamField path="showRowDividers" type="bool" default="false">
  Must match the table's `showRowDividers` prop so the scroll offset accounts for divider height.
</ParamField>

## Column Properties

### Required

<ParamField path="id" type="String" required>
  A unique identifier for the column.
</ParamField>

<ParamField path="label" type="String" required>
  The display text for the column header.
</ParamField>

### Layout

<ParamField path="flex" type="int" default="1">
  Relative width of the column. Higher values take proportionally more space.
</ParamField>

<ParamField path="minWidth" type="double">
  Minimum width in logical pixels. The column will not shrink below this value regardless of flex.
</ParamField>

### Sorting

<ParamField path="isSortable" type="bool" default="false">
  Whether clicking this column's header triggers `NeoTable.onSortChanged`.
</ParamField>

### Reordering / Visibility

<ParamField path="isReorderable" type="bool" default="true">
  Whether this column can be dragged when `NeoTable.isColumnReorderable` is `true`.
</ParamField>

<ParamField path="isHideable" type="bool" default="true">
  Whether this column can be included in `NeoTable.hiddenColumnIds`.
</ParamField>

## Row Properties

### Required

<ParamField path="id" type="String" required>
  A unique identifier for the row.
</ParamField>

<ParamField path="cells" type="Map<String, Widget>" required>
  A map from column ID to the cell widget. Every visible column must have a corresponding entry.
</ParamField>

## Enums

### NeoTableSortDirection

Sort direction for a column. The absence of a sort state (i.e. `null` on the table) represents the unsorted state — there is no `none` value.

* `ascending`: Sort from lowest to highest.
* `descending`: Sort from highest to lowest.

## Best Practices

* **Cell map keys**: Use the exact column `id` strings as `cells` map keys. The table asserts in debug mode if any visible column's ID is missing from a row's cells.
* **Width constraints**: Always provide a bounded width constraint (via `Expanded`, `SizedBox`, etc.) so the table's flex layout has a finite extent to divide.
* **Scroll to row**: Pass a `scrollController` whenever you plan to call `NeoTable.scrollToRow`.
* **Reordering and sorting**: Enabling both `isRowReorderable` and a live `sortState` at the same time can produce confusing UX — the reordered position conflicts with the sorted order. Consider disabling sorting while reordering is active.

## Integration Notes

* The table validates that all row IDs are unique and that every visible column has a matching cell in each row, both via debug-mode assertions.
* Sort cycling for a sortable column: descending → ascending → `null` (unsorted). When a `defaultSortState` is set for the column, the cycle is descending → ascending only.
