Tables and cells

Create rectangular tables, edit values, add or delete rows and columns, and format cell bodies.

Use Body.tables or Range.tables to discover tables in the addressed scope. Table, TableRow, and TableCell are proxies with separate collections. You can edit rectangular tables, merge cells, and split them. Structural edits can fail on tables with an unsupported structure.

Text in a vertical merge over three or more protected body rows can continue across pages. Each row retains its own fixed or minimum height. The text and cached field values retain the original head cell address on every page. Center and bottom alignment apply within each page fragment. This path supports nonoverlapping merges over the same rows, horizontal text, and supported paragraph and run formatting. Drawings, notes, nested tables, manual breaks, floating tables, and unknown content retain their existing layout. If the last row of the merge has a minimum height, it grows to hold the remaining text. If that row cannot split, it moves to the next page. If it is also taller than a page, it starts on a new page and then splits. Exact spans that cannot hold the complete text on their page fragments retain clipping.

Imported rows with omitted leading or trailing grid cells retain borders on their exposed sides. Explicit cell borders take precedence over table borders. This rendering support does not expand the supported structural editing operations.

Create a runtime with Runtime and setup. Use context and document proxies inside runtime.run().

Imported cell borders can include w:tl2br and w:tr2bl diagonals. Table styles can supply them. Direct nil or none clears an inherited diagonal. Diagonals use the cell rectangle, including a vertical merge, in the editor and PDF export. Each repeated header or page fragment uses its own rectangle. A continuous diagonal across page fragments is not supported. Diagonals do not change cell size or text placement. The border toolbar and HTML clipboard do not add or transfer diagonal borders.

Insert a table

Range.insertTable(rowCount, columnCount, location, values?) accepts Before or After. Counts must be positive integers. Supplied values must form a matching rectangular string matrix. Resolve the range, create the table, then configure it after sync:

await runtime.run(async (context) => {
  const paragraph = context.document.paragraphs.getFirst();
  paragraph.load('text');
  await context.sync();
  const anchor = paragraph.getRange('Content');
  await context.sync();
  const table = anchor.insertTable(2, 2, 'After', [
    ['Item', 'Amount'],
    ['License', '$100'],
  ]);
  await context.sync();
  table.headerRowCount = 1;
  await context.sync();
});

The returned Table requires a completed sync before dependent operations. Table insertion is not a Body.insertTable() operation in this subset. Use a body paragraph's range as the anchor.

Read table values and cells

Load scalar table properties explicitly:

await runtime.run(async (context) => {
  const table = context.document.body.tables.getFirstOrNullObject();
  table.load(['values', 'rowCount', 'columnCount']);
  await context.sync();
  if (table.isNullObject) return;
  console.log(table.values);

  const cell = table.getCell(0, 0);
  cell.load('value');
  await context.sync();
  console.log(cell.value);
});

Table.getCell(rowIndex, cellIndex) uses zero-based indexes. Table.rows returns TableRowCollection; TableRow.cells returns TableCellCollection. All three table collections expose items, getFirst(), and getFirstOrNullObject(). Load collection membership before reading item properties.

TableCell.body exposes the cell's Body for text, paragraphs, search, and formatting. Resolve the cell and its body before dependent navigation. To preserve rich cell content, edit an exact range within that body. Whole-cell replacement supports ordinary single-paragraph text and preserves its existing font formatting. The runtime rejects multiline values, nested blocks, and complex cell content. Cell-body font reads remain scoped to that cell. You can navigate existing nested rectangular tables, but cannot create them.

Edit values and structure

MemberBehavior
Table.valuesRead or replace the table's string matrix
TableCell.valueRead or replace one cell's plain text
Table.rowCount, Table.columnCountLoaded, read-only dimensions
Table.addRows(location, rowCount, values?)Add rows at Start or End; return TableRowCollection
TableRow.insertRows(location, rowCount, values?)Add rows Before or After this row; return TableRowCollection
Table.addColumns(location, columnCount, values?)Add columns at Start or End; return void
Table.deleteRows(rowIndex, rowCount?)Delete rows from a zero-based index; default count is one
Table.deleteColumns(columnIndex, columnCount?)Delete columns from a zero-based index; default count is one
Table.delete()Delete the table and clean up its references

Value matrices must match the affected row and column dimensions. Do not assume ragged matrices or implicit resizing work. Deleting all rows or all columns removes the table. Table.deleteRows() works on merged tables. If a vertical merge starts in a deleted row, the next row starts the merge. If no later row continues the merge, the next row is no longer merged. If the row above the deleted row has a merged cell over the same grid columns, the next row joins that merge instead. Row deletion also works on tables whose rows have different cell counts. The deleted row's content is removed. In TrackMineOnly mode, merges change only when the deletion is accepted. If a cell or row cannot be placed in the table grid, the deletion refuses with InvalidArgument. Table.delete() can remove an existing merged table. Deletion can still fail if the table is protected or its surrounding structure is unsupported. Sync structural changes before addressing newly added cells or relying on changed indexes.

Row insertion preserves unrelated merged headers when the source row has ordinary cells covering the table grid. Each new cell, in a row or a column, copies its source cell's paragraph formatting. Values written into it take the source paragraph mark's formatting. The source cell's text runs do not set it, so a bold label at the start of a source cell does not make new values bold. addRows() uses the first or last row as its source. insertRows() uses the addressed row. Merged source rows, row offsets, hidden source cells, and boundaries crossing vertical merges refuse with InvalidArgument. Tables with merges require an explicit grid. Other table mutations retain their documented restrictions.

For example, insert before the first ordinary row after a merged header:

const table = context.document.body.tables.getFirst();
table.rows.load('items');
await context.sync();
const source = table.rows.items[1];
if (!source) throw new Error('Expected an ordinary row after the header');
source.insertRows('Before', 1, [['Item', 'Price']]);
await context.sync();

Merge and split cells

MemberBehavior
Table.mergeCells(topRow, firstCell, bottomRow, lastCell)Merge a rectangle of cells into one; return the merged TableCell
TableCell.split(rowCount, columnCount)Split a cell into rows and columns of equal size; return void

mergeCells() takes two cells by zero-based row index and in-row cell index, the way rows.items[r].cells lists them, and merges the rectangle they span. A merged cell counts once in its row. bottomRow must not be above topRow, but either cell can lie further left. The rectangle grows to include whole merged cells. If a cell crosses the rectangle's edge, or the rectangle holds only one cell, the merge refuses with InvalidArgument and the diagnostic code edit.table-merge-not-rectangle. The merged cell keeps the content of every cell that had content, in reading order. You can edit the returned cell in the same sync.

split() takes counts from 1 through 63. New columns get equal widths, and cells in other rows widen so they keep their width. A cell that is merged over several rows splits only into a number of rows that divides its row count. Splitting a one-row cell into rows adds rows to the table, and the other cells of that row stay merged across the new rows. The first new cell keeps the content. Counts the cell cannot take refuse with InvalidArgument and the diagnostic code edit.table-split-unsupported, for example columns narrower than the minimum column width.

For example, build a table with a merged header row:

const table = context.document.body.getRange('End').insertTable(3, 3, 'After', [
  ['Quarterly results', '', ''],
  ['Region', 'Q1', 'Q2'],
  ['North', '10', '12'],
]);
await context.sync();
const header = table.mergeCells(0, 0, 0, 2);
header.value = 'Quarterly results';
header.verticalAlignment = 'Center';
await context.sync();

Merge and split have no tracked form. In TrackAll or TrackMineOnly mode, both refuse with NotSupported. Cells with a pending cell revision refuse with NotImplemented.

In collaboration, merge and split synchronize, undo, and survive reconnect and reopen. Text that a participant types into an empty cell while another participant merges it away is discarded. Concurrent merges, splits, and column edits of one table converge, but they can leave a row that ends before the last grid column. Column edits keep that row ending where it does. A merge or split that needs the missing part of that row refuses. Merged tables still accept addRows(), insertRows(), addColumns() without values, deleteRows(), and deleteColumns(). A deleted column narrows a merged cell that spans it.

Format tables and cells

Table.style uses an existing table style name. Table.headerRowCount sets the number of leading header rows, from zero through the row count. Zero removes the header flags. Cell formatting exposes these properties:

TableCell propertyDomain
columnWidthSupported finite width in points
shadingColorSix-digit RGB hex, optional #; auto removes direct shading
verticalAlignmentTop, Center, or Bottom; the runtime rejects Mixed on writes

VerticalAlignment exports the corresponding enum values. For character or paragraph formatting, use cell.body.font or cell.body.paragraphs. A width write changes the entire grid column, matching cell widths, and total table width. It also sets fixed table layout. The runtime rejects width changes for tables with missing grids or merged cells.

For example, format a resolved header cell:

cell.shadingColor = '#EEEEEE';
cell.verticalAlignment = 'Center';
await context.sync();

Handle unsupported structures

Structural edits can fail on legacy horizontal merges, protected content, nested preserved structures, and pending revisions. Value matrices and column widths refuse on tables with merged cells. Adding columns or deleting some columns requires an existing table grid. Deleting all columns can still remove a table without a grid. A matching TypeScript signature does not guarantee an edit works on every table. Table value replacement does not silently discard unsupported cell content. Inspect and edit a smaller cell-body range when that preserves the intended structure.

Complete table insertion, cell value replacement, row additions, and partial row deletions support native revisions. An author can configure a complete proposed table while it has no foreign revisions. Existing table properties and columns require permanent edits. Tracked table and cell value replacement refuse in collaboration. In collaboration, a tracked replacement must remain within one paragraph. Tracked text and font edits can operate inside eligible cell paragraphs. A pending tracked row insertion or deletion blocks tracked text edits throughout that row. See Tracked changes.

Each row or column addition or deletion must run alone in its sync, even across separate tables. Do not combine it with cell values or formatting. Separate other dependent structural operations into distinct syncs. If a batch returns ConflictingChanges, read the table again before retrying a revised operation.

Row suggestions

Set document.changeTrackingMode = 'TrackMineOnly' before Table.addRows(), TableRow.insertRows(), or Table.deleteRows(). Added rows can include initial cell values. The runtime saves native w:trPr/w:ins and w:trPr/w:del revisions.

Save and reopen preserve the suggestions. Formatting inside the author's own proposed row records no separate revision, because rejecting the row removes its formatting too. Use body.revisions.acceptAll() or body.revisions.rejectAll() to resolve complete tracked rows. The revision collection omits row revisions from items.

Delete fewer than all rows when you suggest a deletion. Deleting every row refuses in tracking mode. Changing columns and table properties requires an eligible complete proposed table. Row insertion retains the source-row and vertical-merge limits described earlier. The runtime never changes tracking mode to complete an unsupported edit.

Each deletion must leave a row without a pending deletion. Existing deletion suggestions do not count as surviving rows. Independent concurrent deletions can combine to mark every row. Bulk accept and reject resolve that combined result after save and reopen.

Tracked deletion refuses rows with pending row or cell structure revisions with NotImplemented. Resolve those revisions before suggesting another deletion.

Next steps

See Batching, loading, and errors and API member directory.