# 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.

This standalone Vanilla recipe uses the project from [Build your first custom control](/editor/custom-ui/controller-setup).
Save this document as `public/contract.docx`:

[Download the table sample](/fixtures/getting-started.docx): Statement of work with a milestone table · DOCX


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

Replace the contents of `<body>` in `index.html` with the table actions, download button, and 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>
  <button id="download" type="button" disabled>Download DOCX</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]

Replace `src/main.ts`. The example reads `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');
const downloadButton = document.querySelector<HTMLButtonElement>('#download');

if (!tablePosition || !addRowButton || !deleteRowButton || !tableStatus || !downloadButton) {
  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 }) => {
    downloadButton.disabled = false;
    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);
    };
  },
});

downloadButton.addEventListener('click', async () => {
  downloadButton.disabled = true;
  try {
    const file = await superdoc.export({ triggerDownload: true });
    tableStatus.textContent = file ? 'DOCX downloaded.' : 'The document could not be exported.';
  } catch (error) {
    console.error('Could not export the document.', error);
    tableStatus.textContent = 'Could not export the document. Try again.';
  } finally {
    downloadButton.disabled = false;
  }
});

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 the milestone table, add a row, delete a row, and choose **Download DOCX**. Outside the table, both row 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.
