Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.superdoc.dev/llms.txt

Use this file to discover all available pages before exploring further.

Turn off SuperDoc’s built-in chrome, listen for the active control, and anchor your own UI over it. The control wrappers and data-sdt-* attributes stay in the DOM, so your UI has something to attach to.

A minimal field chip

import { SuperDoc } from 'superdoc';
import { createSuperDocUI } from 'superdoc/ui';

new SuperDoc({
  selector: '#editor',
  document: '/contract.docx',
  // Turn off the built-in labels, borders, and hover/selection chrome.
  modules: { contentControls: { chrome: 'none' } },
  onReady: ({ superdoc }) => {
    const ui = createSuperDocUI({ superdoc });

    superdoc.on('content-control:active-change', ({ active }) => {
      if (!active) return chip.hide(); // `chip` is your own element
      const rect = ui.contentControls.getRect({ id: active.id });
      if (rect.success) chip.showAt(rect.rect, active.alias ?? active.tag);
    });
  },
});
The event tells you what is active; getRect tells you where to draw. active is an SdtRef with id, tag, alias, controlType, and scope.

Pick the right surface

GoalAPI
Active control (enter, switch, leave)superdoc.on('content-control:active-change')
Click inside a controlsuperdoc.on('content-control:click')
Full live list and active stackui.contentControls.observe() / getSnapshot()
Read one controlui.contentControls.get({ id })
Position your UIui.contentControls.getRect({ id })
Scroll a control into viewui.contentControls.scrollIntoView({ id })
Scroll to it and put the cursor inui.contentControls.focus({ id })
Re-anchor your UI when the page movesui.viewport.observe(() => ...)
Hover and right-click hit-testingui.viewport.entityAt() / contextAt()
Change content, tags, or lockseditor.doc.contentControls.*
active is the innermost control. For nested controls (an inline field inside a block clause), activePath carries the full stack, innermost first, so you don’t also need observe() just to read the nesting. scrollIntoView resolves the control’s position from the document, so it works even when the control is on a page that hasn’t rendered yet (the page mounts, then scrolls). It scrolls only - it does not move the cursor into the control. focus does both: scrolls to the control and places the caret inside so the user can start typing. focus is selection, not editing - it does not bypass lock or document-mode rules, so a locked or read-only control can be focused for inspection but edits are still blocked. ui.viewport.observe is the single signal for “your getRect() coordinates may be stale, re-query”: it fires (coalesced, once per frame) on scroll, resize, zoom, and layout reflow, so an overlay anchored with getRect stays glued without hand-wiring those events yourself.

How the model works

You build your UI over the control, not inside it. SuperDoc owns how the control’s content is painted in the document; you turn off its built-in chrome and draw your own (chips, badges, panels) anchored with getRect, react with the events, and change content through editor.doc.contentControls.*. Custom field types are expressed as a tag - for example { kind: 'smartField', key: 'party_name' }, interpreted by your own UI - the underlying control stays a standard Word SDT so it round-trips to .docx.

See also