> For the complete documentation index, see [llms.txt](https://docs.rowsncolumns.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.rowsncolumns.app/configuration/features/outline-grouping.md).

# Outline & Grouping

Outlining lets users collapse and expand contiguous ranges of rows or columns into nested levels — the same `Shift + Alt + →` mechanic from Excel. The state lives on `DimensionProperties` inside each sheet, the actions are exposed as hooks on `useSpreadsheetState`, and a pair of optional render components (`RowOutlineGutter`, `ColumnOutlineGutter`) paint the +/- buttons and bracket lines next to the canvas.

Outline state round-trips through both the XLSX and ODS importer/exporter, so a sheet grouped in your app opens grouped in Excel / LibreOffice and vice versa.

## Data model

Group state lives on the sheet's `rowMetadata` / `columnMetadata` arrays. Both arrays are 1-indexed (index `0` is reserved) and entries are `DimensionProperties`:

```ts
type DimensionProperties = {
  // Depth of the group this row/column belongs to (1..7).
  outlineLevel?: number;
  // True on a summary cell whose group is currently collapsed —
  // controls whether the gutter shows + or -.
  collapsed?: boolean;
  // True on a child cell when its group is collapsed. Distinct from
  // `hiddenByUser` so an outline expand doesn't unhide a manually
  // hidden row.
  hiddenByGroup?: boolean;
  // ...sizing/visibility flags
};
```

Sheet-level direction settings live in `sheet.outlinePr`:

```ts
type OutlinePr = {
  // Where the row summary sits relative to its group.
  //   true  (Excel default): summary is BELOW the group
  //   false:                  summary is ABOVE the group
  summaryBelow?: boolean;
  // Where the column summary sits.
  //   true  (Excel default): summary is to the RIGHT of the group
  //   false:                  summary is to the LEFT
  summaryRight?: boolean;
};
```

A typical level-1 row group with the level-1 summary sitting at row 6 looks like:

```ts
sheet.rowMetadata[3] = { outlineLevel: 1 };
sheet.rowMetadata[4] = { outlineLevel: 1 };
sheet.rowMetadata[5] = { outlineLevel: 1 };
// row 6 is the summary — left at outlineLevel 0
```

Collapsing that group then sets:

```ts
sheet.rowMetadata[3].hiddenByGroup = true;
sheet.rowMetadata[4].hiddenByGroup = true;
sheet.rowMetadata[5].hiddenByGroup = true;
sheet.rowMetadata[6].collapsed = true; // summary shows the "+" button
```

Nested groups stack the level — e.g. cols 3–4 inside a level-1 group spanning 2–5 carry `outlineLevel: 2`. The maximum depth is **7** (matches Excel).

## State hooks

`useSpreadsheetState` exposes nine callbacks. All of them undo/redo via the same patch history the rest of the spreadsheet uses, and emit a matching `onCommand` event so you can mirror the action to a backend.

| Callback                | Signature                                   | Behavior                                                                                                                                                                     |
| ----------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onGroupRows`           | `(sheetId, rowIndexes: number[])`           | Increments `outlineLevel` on the selected rows. Non-contiguous selections produce one group per contiguous span.                                                             |
| `onGroupColumns`        | `(sheetId, columnIndexes: number[])`        | Same, for columns.                                                                                                                                                           |
| `onUngroupRows`         | `(sheetId, rowIndexes: number[])`           | Decrements `outlineLevel`. Once a row's level reaches 0 the `collapsed` / `hiddenByGroup` flags are cleared too.                                                             |
| `onUngroupColumns`      | `(sheetId, columnIndexes: number[])`        | Same, for columns.                                                                                                                                                           |
| `onCollapseRowGroup`    | `(sheetId, summaryRowIndex)`                | Hides every row in the group that summary belongs to (respects `outlinePr.summaryBelow`).                                                                                    |
| `onCollapseColumnGroup` | `(sheetId, summaryColumnIndex)`             | Hides every column in the group that summary belongs to.                                                                                                                     |
| `onExpandRowGroup`      | `(sheetId, summaryRowIndex)`                | Reverses a collapse. Manual `hiddenByUser` flags on individual rows survive the expand.                                                                                      |
| `onExpandColumnGroup`   | `(sheetId, summaryColumnIndex)`             | Reverses a column collapse.                                                                                                                                                  |
| `onSetOutlineDepth`     | `(sheetId, axis: "row" \| "column", depth)` | Excel-style 1 / 2 / 3 selector. `depth=1` collapses every group, `depth=maxLevel+1` expands every group, intermediate values keep outer levels open and collapse inner ones. |

```tsx
import { useSpreadsheetState } from "@rowsncolumns/spreadsheet-state";

const {
  onGroupRows,
  onUngroupRows,
  onCollapseRowGroup,
  onExpandRowGroup,
  onGroupColumns,
  onUngroupColumns,
  onCollapseColumnGroup,
  onExpandColumnGroup,
  onSetOutlineDepth,
} = useSpreadsheetState({ sheets, sheetData, ... });
```

## Rendering the gutter

`@rowsncolumns/spreadsheet` ships two memoised render components — `RowOutlineGutter` and `ColumnOutlineGutter` — that paint the +/- buttons, bracket lines, and 1/2/3 level selector. They are siblings of the `CanvasGrid` (not children); each gutter is a no-op when the sheet has no groups on its axis, so it's safe to always render them.

```tsx
import {
  CanvasGrid,
  ColumnOutlineGutter,
  RowOutlineGutter,
} from "@rowsncolumns/spreadsheet";

<div className="flex flex-col h-full">
  <ColumnOutlineGutter
    sheetId={activeSheetId}
    columnMetadata={columnMetadata}
    rowMetadata={rowMetadata}
    onCollapseColumnGroup={onCollapseColumnGroup}
    onExpandColumnGroup={onExpandColumnGroup}
    onSetOutlineDepth={onSetOutlineDepth}
  />
  <div className="flex flex-1 min-h-0 min-w-0">
    <RowOutlineGutter
      sheetId={activeSheetId}
      rowMetadata={rowMetadata}
      onCollapseRowGroup={onCollapseRowGroup}
      onExpandRowGroup={onExpandRowGroup}
      onSetOutlineDepth={onSetOutlineDepth}
    />
    <CanvasGrid /* ...usual props */ />
  </div>
</div>
```

Why siblings, not children? `CanvasGrid` relies on its wrapper (`.rnc-canvas-wrapper`) being the `position: relative` ancestor for active-cell and selection overlays. Wrapping or nesting the canvas inside the gutter would break that anchor.

### Layout details

* The column gutter is a horizontal strip above the canvas; its height = `(maxColumnOutlineLevel + 1) * 20 + 4` px.
* The row gutter is a vertical strip to the left of the canvas; its width = `(maxRowOutlineLevel + 1) * 20 + 4` px.
* `computeOutlineGutterSize(metadata)` returns the same value if you need to inset something else.
* The column gutter scrolls horizontally in lockstep with the canvas (via the `scrollSubscriber` signal); the row gutter scrolls vertically.

### Direction (summaryBelow / summaryRight)

Pass the sheet's direction through to flip where the +/- button sits:

```tsx
<RowOutlineGutter
  ...
  summaryBelow={sheet.outlinePr?.summaryBelow !== false}
/>
<ColumnOutlineGutter
  ...
  summaryRight={sheet.outlinePr?.summaryRight !== false}
/>
```

Both default to `true` (Excel's default).

## Keyboard shortcuts

Outline shortcuts are wired through the same `keyboard-handler` the rest of the canvas uses:

| Shortcut          | Action                          |
| ----------------- | ------------------------------- |
| `Shift + Alt + →` | Group selected rows / columns   |
| `Shift + Alt + ←` | Ungroup selected rows / columns |

## Import / Export

| Format | Import | Export | Round-trip tests                                                    |
| ------ | ------ | ------ | ------------------------------------------------------------------- |
| XLSX   | ✅      | ✅      | `libs/toolkit/excel-parser/__tests__/outline-roundtrip.spec.ts`     |
| ODS    | ✅      | ✅      | `libs/toolkit/excel-parser/__tests__/ods-outline-roundtrip.spec.ts` |

### XLSX details

* Children: `<row outlineLevel="N" hidden="1" collapsed="…">`, `<col outlineLevel="N" …>`.
* Sheet direction: `<sheetPr><outlinePr summaryBelow="…" summaryRight="…"/></sheetPr>` — only emitted when the sheet diverges from Excel's defaults.
* `hiddenByGroup` is encoded as `hidden="1"` + `outlineLevel > 0`; the parser routes it back into `hiddenByGroup` rather than `hiddenByUser` so a user-driven hide isn't conflated with a group hide.

### ODS details

* Groups are wrapped in `<table:table-row-group>` / `<table:table-column-group>` elements, nested for level > 1.
* A collapsed group carries `table:display="false"`; rows/columns inside such a wrapper get `hiddenByGroup: true`, and the summary cell (per `outlinePr.summaryBelow` / `summaryRight`) gets `collapsed: true`.
* ODS has no native equivalent of `<outlinePr>`. Non-default `summaryBelow` / `summaryRight` are **not** persisted through an ODS round-trip — the sheet reopens with Excel defaults.

## Notes & gotchas

* **Maximum depth is 7.** `onGroupRows` / `onGroupColumns` clamp at level 7; further `Group` actions on already-deep rows are no-ops.
* **Manual hide survives an expand.** `useOnExpandRowGroup` / `useOnExpandColumnGroup` only clear `hiddenByGroup`; `hiddenByUser` from `onHideRow` / `onHideColumn` is preserved. This is better than Excel, which conflates the two.
* **Depth selector and nested groups.** `onSetOutlineDepth` decides hiding from each cell's own `outlineLevel`, so a level-2 child stays hidden when the surrounding level-1 group expands. (Earlier versions wrote `hiddenByGroup` on whole spans during the scan, which caused the outer-group expand to clobber the inner-group collapse.)
* **Overlapping groups are not supported.** The data model uses `outlineLevel` as a per-cell depth, not a group ID, so groups must be **nested or disjoint** — same constraint Excel has. Re-grouping a range that overlaps an existing group nests it inside the larger group.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.rowsncolumns.app/configuration/features/outline-grouping.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
