# Build a content-control panel

> List document fields, move people to them, and update text controls from application-owned UI.



Content controls are structured fields stored in a DOCX. Use `ui.contentControls` to keep an application-owned panel synchronized with the Editor. Use the Document API to change a control's content or properties.

If SuperDoc should render the control chrome instead, see [structured content](/editor/built-in-ui/structured-content).

Complete [Custom UI controller setup](/editor/custom-ui/controller-setup) first. This example expects `/contract.docx` to contain at least one plain-text content control.

The [content controls example](https://go.superdoc.dev/examples/content-controls) provides a complete project and a synthetic DOCX containing one text control.

## Add the field panel [#add-the-field-panel]

Create a status message, field list, and Editor container:

```html
<aside aria-labelledby="controls-heading">
  <h2 id="controls-heading">Document fields</h2>
  <p id="controls-status" role="status">Loading content controls…</p>
  <ul id="control-list"></ul>
</aside>

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

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

```

## Bind navigation and editing [#bind-navigation-and-editing]

Read the Editor's controller, `superdoc.ui`, after the Editor and Document API are ready:

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

const controlList = document.querySelector<HTMLUListElement>('#control-list');
const controlsStatus = document.querySelector<HTMLParagraphElement>('#controls-status');

if (!controlList || !controlsStatus) {
  throw new Error('The content-control UI is incomplete.');
}

let stopContentControls: (() => void) | null = null;

const superdoc = new SuperDoc({
  selector: '#editor',
  document: '/contract.docx',
  onReady: ({ superdoc: readySuperDoc }) => {
    const doc = readySuperDoc.activeEditor?.doc;
    if (!doc) throw new Error('The Document API is not ready.');

    const ui = readySuperDoc.ui;

    const focusControl = async (id: string) => {
      const result = await ui.contentControls.focus({ id, block: 'center', behavior: 'smooth' });
      if (!result.success) controlsStatus.textContent = `The field is ${result.reason}.`;
    };

    const updateTextControl = async (id: string, value: string) => {
      const control = ui.contentControls.get({ id });
      if (!control || control.controlType !== 'text') {
        controlsStatus.textContent = 'This field is no longer an editable text control.';
        return;
      }

      const receipt = await doc.contentControls.text.setValue({ target: control.target, value });
      if (!receipt.success) {
        controlsStatus.textContent = receipt.failure.message;
        return;
      }

      controlsStatus.textContent = 'Field updated.';
    };

    const render = (controls: ContentControlsSlice) => {
      controlList.replaceChildren();
      controlsStatus.textContent =
        controls.status === 'pending' ? 'Loading content controls…' : `${controls.total} document fields`;

      for (const control of controls.items) {
        const row = document.createElement('li');
        const label = document.createElement('span');
        const show = document.createElement('button');

        label.textContent = control.properties.alias ?? control.properties.tag ?? control.controlType;
        show.type = 'button';
        show.textContent = controls.activeIds.includes(control.id) ? 'Focused' : 'Show';
        show.addEventListener('click', () => void focusControl(control.id));
        row.append(label, show);

        if (control.controlType === 'text') {
          const value = document.createElement('input');
          const update = document.createElement('button');

          value.type = 'text';
          value.value = control.text ?? '';
          value.setAttribute('aria-label', `Value for ${label.textContent}`);
          update.type = 'button';
          update.textContent = 'Update';
          update.addEventListener('click', () => void updateTextControl(control.id, value.value));
          row.append(value, update);
        }

        controlList.append(row);
      }
    };

    ui.contentControls.list();
    stopContentControls = ui.contentControls.observe(render);
  },
});

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

```

The example uses two public surfaces for different jobs:

* `ui.contentControls` observes the loaded list, reports controls at the current selection, focuses a known control, and keeps navigation aligned with the document canvas.
* `doc.contentControls.text.setValue()` performs the document mutation and returns the receipt.

Do not find controls by querying rendered DOM attributes. The list item contains the stable control ID, mutation target, type, lock mode, properties, and an optional selection target.

## Check the control type before editing [#check-the-control-type-before-editing]

`text.setValue()` applies to plain-text controls. Date, checkbox, choice-list, repeating-section, and rich-text controls have different operations and constraints. Branch on `controlType` and expose only actions that match the current control.

Read `lockMode` before presenting an edit affordance, but still inspect the mutation receipt. The document can change between the list read and the update.

## Keep the panel reactive [#keep-the-panel-reactive]

Call `list()` when opening the panel to request the catalog, then keep `observe()` active. The snapshot reports `pending`, `stale`, or `ready` state as the document loads and changes.

`focus()` scrolls to the control and places the caret inside it when the host can resolve its selection target. It does not bypass a content lock or viewing mode. Focus is navigation. The later mutation still decides whether editing is allowed.

Verify the workflow by updating one plain-text control, exporting the DOCX, and reopening it. The new value should remain attached to the same content control.

Continue with [Load and save documents](/editor/load-and-save-documents). Use the generated Document API reference for checkbox, date, choice-list, metadata, binding, and structural operations.
