# Build selection-aware table controls

> Enable application-owned table actions only when the current selection resolves to a table cell.



A DOCX table does not require custom UI. Build these controls only when your application needs actions outside the
document that follow the active cell, such as **Add row** in an application toolbar. Use `ui.tables` to read that
selection context and `ui.commands` to run actions against it.

If your application already has an explicit table target, use the
[Document API tables reference](/document-api/reference/tables/) instead.

Start from [Build your first custom control](/editor/custom-ui/controller-setup). 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 controls [#verify-the-controls]

Place the caret in a table, add a row, delete a row, and export the DOCX. Outside the table, both buttons should disable
with a contextual reason.

Use the [Document API tables reference](/document-api/reference/tables/) for explicit targets or operations that are not
selection-driven. Return to the [Custom UI overview](/editor/custom-ui/overview) to choose another workflow.
