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.
Complete Custom UI controller setup first. This example expects /contract.docx to contain at least one plain-text content control.
The content controls example provides a complete project and a synthetic DOCX containing one text control.
Add the field panel
Create a status message, field list, and Editor container:
<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
Read the Editor's controller, superdoc.ui, after the Editor and Document API are ready:
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.contentControlsobserves 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
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
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. Use the generated Document API reference for checkbox, date, choice-list, metadata, binding, and structural operations.