# Build contextual table controls

> Enable application-owned table actions from the current cell and explain why an action is unavailable.



Table commands depend on the current Editor selection. Use `ui.tables` to read the active table context and `ui.commands` to run actions against it.

Complete [Custom UI controller setup](/editor/custom-ui/controller-setup) first. This example expects `/contract.docx` to contain a table.

## Add the controls [#add-the-controls]

Create two table actions, context output, status output, and an Editor container:

```html
<aside aria-labelledby="table-heading">
  <h2 id="table-heading">Table controls</h2>
  <p id="table-position" role="status">Place the caret in a table.</p>
  <button id="add-row" type="button" disabled>Add row below</button>
  <button id="delete-row" type="button" disabled>Delete row</button>
  <p id="table-status" role="status"></p>
</aside>

<div id="editor" style="height: 70vh"></div>

<script type="module" src="/src/main.ts"></script>

```

## Bind actions to table context [#bind-actions-to-table-context]

Read the Editor's controller, `superdoc.ui`, after the Editor is ready:

```ts
import { SuperDoc } from 'superdoc';
import type { CommandExecutionResult } from 'superdoc/ui';
import 'superdoc/style.css';

const tablePosition = document.querySelector<HTMLParagraphElement>('#table-position');
const addRowButton = document.querySelector<HTMLButtonElement>('#add-row');
const deleteRowButton = document.querySelector<HTMLButtonElement>('#delete-row');
const tableStatus = document.querySelector<HTMLParagraphElement>('#table-status');

if (!tablePosition || !addRowButton || !deleteRowButton || !tableStatus) {
  throw new Error('The table controls are incomplete.');
}

let stopAddRow: (() => void) | null = null;
let stopDeleteRow: (() => void) | null = null;
let removeHandlers: (() => void) | null = null;

const superdoc = new SuperDoc({
  selector: '#editor',
  document: '/contract.docx',
  onReady: ({ superdoc: readySuperDoc }) => {
    const ui = readySuperDoc.ui;
    const addRow = ui.commands.get('table-add-row-after');
    const deleteRow = ui.commands.get('table-delete-row');

    const render = () => {
      const context = ui.tables.getContext();
      const addState = addRow.getState();
      const deleteState = deleteRow.getState();

      tablePosition.textContent = context.inTable
        ? `Row ${(context.rowIndex ?? 0) + 1}, column ${(context.columnIndex ?? 0) + 1}`
        : 'Place the caret in a table.';
      addRowButton.disabled = !addState.enabled;
      deleteRowButton.disabled = !deleteState.enabled;
      tableStatus.textContent = addState.reason ?? deleteState.reason ?? '';
    };

    const report = (result: CommandExecutionResult, successMessage: string) => {
      if (result === false) {
        tableStatus.textContent = 'The table action is unavailable.';
        return;
      }
      if (typeof result === 'object' && !result.success) {
        tableStatus.textContent = result.failure.message;
        return;
      }
      tableStatus.textContent = successMessage;
    };

    const insertRow = async () => report(await addRow.executeAsync(), 'Row added.');
    const removeRow = async () => report(await deleteRow.executeAsync(), 'Row deleted.');

    stopAddRow = addRow.observe(render);
    stopDeleteRow = deleteRow.observe(render);
    addRowButton.addEventListener('click', insertRow);
    deleteRowButton.addEventListener('click', removeRow);

    removeHandlers = () => {
      addRowButton.removeEventListener('click', insertRow);
      deleteRowButton.removeEventListener('click', removeRow);
    };
  },
});

window.addEventListener('beforeunload', () => {
  stopAddRow?.();
  stopDeleteRow?.();
  removeHandlers?.();
  superdoc.destroy();
});

```

The example does not construct table locators. The controller resolves the enclosing table, row, column, and cell from the live selection. It then builds the Document API input for each command.

## Render command state, not assumptions [#render-command-state-not-assumptions]

Each table command reports whether it is supported and enabled. Outside a table, contextual actions fail closed with `table-context-unavailable`. In viewing mode, mutations report `document-readonly`.

Use `getState()` for the initial render and `observe()` for later selection, mode, and capability changes. Still inspect the result from `executeAsync()`. Context can change between rendering a button and clicking it.

The routed table command family includes inserting or deleting rows and columns, deleting a table, merging cells, splitting a cell, and removing borders. Add only the controls your workflow needs. Do not render every catalog command as a toolbar.

## Keep context tied to selection [#keep-context-tied-to-selection]

`ui.tables.getContext()` returns the current table ID, row and column indices, cell ID, and dimensions when the host can resolve them. Treat that snapshot as presentation state. Do not cache it as a durable mutation target or derive table identity from painted DOM attributes.

Verify the workflow by placing the caret in a table, adding a row, deleting a row, and exporting the DOCX. Outside the table, both buttons should disable with a contextual reason.

Use the Document API tables reference when application code already has an explicit table target or needs an operation that is not selection-driven.
