Composition

Build a Vue DOCX editor with packaged parts, selected toolbar controls, and your own components.

<DocxEditor> combines public components into a packaged editor. Arrange these components yourself, or connect your own controls to the same commands and state.

Arrange editor components

Choose toolbar controls, placement, labels, spacing, and styles. Add optional document controls, then copy the selected Vue components and CSS.

The demo shows a subset of the available components and options. These layouts demonstrate composition; they are not packaged toolbar presets.

Use your own Vue components for toolbars, menus, and panels. Connect them with Vue composables, combine them with packaged parts, and supply your own CSS.

Composition requirements

RequirementWhen requiredPurpose
DocxEditorRootEvery composed editorOwns and provides the editor instance
DocxEditorViewportEvery composed editorSupplies the scroll container and page-layout classes
DocxEditorContentEvery composed editorMounts the painted document surface
Editor stylesheetEvery composed editorStyles the viewport and packaged controls
.docx-editor wrapperCustom controls that use editor stylesSupplies scoped styles and tokens
Container heightEvery composed editorGives the viewport room to render
Remount :keyWhen you change modulesLoads the new modules
Positioned workspace rowWhen you use the navigation paneAnchors the navigation pane
Pro review moduleWhen you use Pro review chromeEnables comments, tracked changes, and custom-node review chrome

If you use Pro review chrome, register its modules once on DocxEditorRoot. For setup, see the Pro package documentation.

Primitives

Every editor needs these three components in this order:

<script setup lang="ts">
import { DocxEditorContent, DocxEditorRoot, DocxEditorViewport } from '@docx-editor.dev/vue';
import '@docx-editor.dev/vue/styles.css';

defineProps<{ bytes: Uint8Array }>();
</script>

<template>
  <div class="docx-editor" style="height: 100vh">
    <DocxEditorRoot :document="bytes">
      <DocxEditorViewport>
        <DocxEditorContent />
      </DocxEditorViewport>
    </DocxEditorRoot>
  </div>
</template>

You can place all optional chrome inside DocxEditorRoot.

Root props

DocxEditorRoot accepts these document-level props:

  • document, fonts, author, locale, mode, and modules
  • zoom and zoomMode
  • translate and tableInteractionLabel
  • imageDecodePort

Listen for @ready, @change, and @font-error on the root.

The root rebuilds for document, fonts, and imageDecodePort identity changes. Changes to author, locale, mode, translate, zoom, zoomMode, and the locale catalog apply without a rebuild. Existing revisions keep their authors. The root ignores a later modules array change until you remount it with a different :key.

locale selects regional date input and generated document labels. It defaults to en-US.

For full prop types, see the Vue API reference.

UI language and date input

DocxEditorRoot reads UI translations from LocaleProvider; it has no i18n prop. Set locale separately for regional date input. This example uses Polish for both:

<script setup lang="ts">
import { defineComponent, h } from 'vue';
import {
  DocxEditorContent,
  DocxEditorRoot,
  DocxEditorToolbar,
  DocxEditorViewport,
  LocaleProvider,
  useChromeTranslate,
} from '@docx-editor.dev/vue';
import { pl } from '@docx-editor.dev/i18n';

defineProps<{ bytes: Uint8Array }>();
const LocalizedToolbar = defineComponent({
  setup() {
    const t = useChromeTranslate();
    return () => h(DocxEditorToolbar, { t: t.value });
  },
});
</script>

<template>
  <LocaleProvider :i18n="pl">
    <DocxEditorRoot :document="bytes" locale="pl-PL">
      <LocalizedToolbar />
      <DocxEditorViewport>
        <DocxEditorContent />
      </DocxEditorViewport>
    </DocxEditorRoot>
  </LocaleProvider>
</template>

Without the provider, UI strings remain English unless an ancestor supplies a catalog. See internationalization for catalog imports and defaults.

provideDocxEditor()

Call provideDocxEditor() when a parent layout holds the editor. The helper provides one editor instance to the returned root and its descendants.

<script setup lang="ts">
import { provideDocxEditor } from '@docx-editor.dev/vue';

const { DocxEditorRoot, rootProps, rootListeners, editorRef } = provideDocxEditor({
  document: bytes,
  onReady,
});
</script>

<template>
  <DocxEditorRoot v-bind="rootProps" v-on="rootListeners">
    <!-- Add chrome and the viewport here. -->
  </DocxEditorRoot>
</template>

editorRef tracks the same instance that useDocxEditor() returns in descendants.

For composed chrome, call useChromeTranslate() inside the locale provider and pass its result through each part's t prop.

Customization options

Choose the amount of customization you need:

  1. Set a class or a --doc-* color token.
  2. Replace a part's icon prop.
  3. Use as-child to merge behavior into your element.
  4. Override one compound part.
  5. Set :preset="false" and arrange all parts.
  6. Build your markup with Vue composables.

Use as-child

as-child merges a part's behavior and accessibility attributes into one child. It does not render a wrapper.

<DocxEditorToolbar.Bold as-child>
  <MyButton variant="ghost">Bold</MyButton>
</DocxEditorToolbar.Bold>

The child must render one element.

Override a slot

A named part replaces the matching part in the preset. The hidden prop removes that part.

<DocxEditorToolbar>
  <DocxEditorToolbar.Bold class="my-bold" />
  <DocxEditorToolbar.Highlight hidden />
</DocxEditorToolbar>

Custom toolbar

Set :preset="false" to include only your chosen parts. Their template order sets the toolbar order. Each part reads command state and disabled reasons from the editor.

This example combines selected packaged parts:

<DocxEditorToolbar :preset="false" class-name="my-toolbar">
  <DocxEditorToolbar.Undo />
  <DocxEditorToolbar.Redo />
  <DocxEditorToolbar.Separator />
  <DocxEditorToolbar.StylePicker />
  <DocxEditorToolbar.FontFamily />
  <DocxEditorToolbar.FontSize />
  <DocxEditorToolbar.Separator />
  <DocxEditorToolbar.Bold />
  <DocxEditorToolbar.Italic />
  <DocxEditorToolbar.FontColor />
  <DocxEditorToolbar.Zoom />
</DocxEditorToolbar>

Most named parts map to one ChromeSlotId. Alignment combines the four alignment.* slots. The chrome slot reference lists every slot and named React and Vue part.

Add a host toolbar action

Use DocxEditorToolbar.Action for an action without a registry slot. Call useEditorCommand() to read whether the command can run.

<script setup lang="ts">
import { computed } from 'vue';
import {
  DocxEditorToolbar,
  useEditorCommand,
  useEditorState,
  type EditorCommand,
} from '@docx-editor.dev/vue';

const command: EditorCommand = {
  type: 'setMarkAttr',
  mark: 'highlight',
  attr: 'val',
  value: 'cyan',
};
const action = useEditorCommand(command);
const collapsed = useEditorState((state) => state.selectionCollapsed);
const enabled = computed(() => action.isEnabled.value && !collapsed.value);
const reason = computed(() =>
  collapsed.value && action.isEnabled.value ? 'Nothing is selected' : action.disabledReason.value
);
</script>

<template>
  <DocxEditorToolbar.Action
    label="Highlight"
    :disabled="!enabled"
    :disabled-reason="reason ?? undefined"
    @select="action.execute"
  />
</template>

Reuse this logic for each surface that exposes the action.

Custom context menu

You can keep packaged rows, remove rows, add registry slots, and add host rows.

<DocxEditorContextMenu class-name="my-menu">
  <DocxEditorContextMenu.Cut />
  <DocxEditorContextMenu.Copy />
  <DocxEditorContextMenu.Slot slot="review.comments" hidden />

  <DocxEditorContextMenu.Row
    :disabled="!enabled"
    :title="reason ?? undefined"
    @select="action.execute"
  >
    Highlight
  </DocxEditorContextMenu.Row>

  <DocxEditorContextMenu.Submenu label-key="my.insert">
    <DocxEditorContextMenu.Row
      @select="editorRef?.exec({ type: 'insertBreak', kind: 'page' })"
    >
      Page break
    </DocxEditorContextMenu.Row>
  </DocxEditorContextMenu.Submenu>

  <DocxEditorContextMenu.Slot slot="format.clear" />
</DocxEditorContextMenu>

DocxEditorContextMenu.Slot gets its label, icon, state, and command from the registry.

Custom menu bar

Pass exporters to the menu to connect export and print handlers. For setup, see Print documents.

DocxEditorMenu derives its default menus from CHROME_MENUS. Override only the menus that your product changes.

<DocxEditorMenu class-name="my-menubar" :open-handler="openFile" :save-handler="saveFile">
  <DocxEditorMenu.File />
  <DocxEditorMenu.Format />
  <DocxEditorMenu.Insert />

  <DocxEditorMenu.Menu id="review" label="Review" :preset="false">
    <DocxEditorMenu.Row :disabled="!enabled" @select="action.execute">
      Highlight passage
    </DocxEditorMenu.Row>
    <DocxEditorMenu.Separator />
    <DocxEditorMenu.Row
      shortcut="Ctrl+Enter"
      @select="editorRef?.exec({ type: 'insertBreak', kind: 'page' })"
    >
      Page break
    </DocxEditorMenu.Row>
  </DocxEditorMenu.Menu>

  <DocxEditorMenu.Help :preset="false">
    <DocxEditorMenu.Row @select="openDocumentation">
      Documentation
    </DocxEditorMenu.Row>
  </DocxEditorMenu.Help>
</DocxEditorMenu>

Use open-handler and save-handler for composed Vue menu actions. <DocxEditor> exposes these actions as @open and @save.

Custom navigation pane

Each navigation part accepts its own class.

<DocxEditorNavigation class-name="my-nav" :toggle="{ className: 'my-nav__toggle' }">
  <DocxEditorNavigation.Header class-name="my-nav__header">
    <DocxEditorNavigation.Close class-name="my-nav__close" />
    <DocxEditorNavigation.Title class-name="my-nav__title" />
  </DocxEditorNavigation.Header>
  <DocxEditorNavigation.Tabs class-name="my-nav__tabs" />
  <DocxEditorNavigation.Headings class-name="my-nav__headings" />
  <DocxEditorNavigation.Find class-name="my-nav__find" />
</DocxEditorNavigation>

The headings list uses the document outline from the editor.

Custom loading screen

DocxEditorLoading covers the time before bytes arrive and the time while a document opens.

<DocxEditorViewport>
  <DocxEditorLoading>
    <div class="my-loading">
      <MySpinner />
      <span>Opening document</span>
    </div>
  </DocxEditorLoading>
  <DocxEditorContent />
</DocxEditorViewport>

Set overlay to cover the previous document while the next document opens.

<div class="workspace">
  <DocxEditorViewport>
    <DocxEditorContent />
  </DocxEditorViewport>
  <DocxEditorLoading overlay />
</div>

Give .workspace a positioning context. Use DocxEditorLoading.Spinner or DocxEditorLoadingSpinner to style the packaged spinner.

DocxEditorHyperLink shows the link target and editing actions.

<DocxEditorHyperLink class-name="my-popover">
  <DocxEditorHyperLink.Copy hidden />
  <DocxEditorHyperLink.Unlink :icon="unlinkIcon" />
</DocxEditorHyperLink>

Its parts are Url, Fields, Edit, Apply, Cancel, Copy, Unlink, and Error. Use useHyperlinkPopup() when you need different markup.

Rulers

The rulers read page setup and zoom from the editor. A drag creates one undo entry when you release the handle.

Use Tab to focus a vertical ruler margin handle. Arrow Up increases the margin; Arrow Down decreases it. Hold Shift to change one twip. Home and End select the permitted limits. Each change creates one undo entry.

<DocxEditorRoot :document="bytes">
  <DocxEditorHorizontalRuler />
  <DocxEditorViewport>
    <DocxEditorVerticalRuler />
    <DocxEditorContent />
  </DocxEditorViewport>
</DocxEditorRoot>

Read-only documents keep the ruler handles inactive.

Page setup dialog

You control DocxEditorPageSetupDialog with open and @close.

<script setup lang="ts">
import { ref } from 'vue';
import { DocxEditorPageSetupDialog } from '@docx-editor.dev/vue';

const pageSetupOpen = ref(false);
</script>

<template>
  <button type="button" @click="pageSetupOpen = true">Page setup</button>
  <DocxEditorPageSetupDialog :open="pageSetupOpen" @close="pageSetupOpen = false" />
</template>

Use usePageSetup() to build a different form.

Page setup, paragraph options, and legacy text field options expose replaceable parts and draft contexts. Import definePopup from @docx-editor.dev/vue and pass your component through the editor's popups map.

Use popups.export for export feedback and popups.print for print feedback. The packaged dialogs are DocxEditor.ExportDialog and DocxEditor.PrintDialog. For examples, see Customize popups.

Content-control panel

DocxEditorContentControl inspects the content control at the caret.

<DocxEditorContentControl>
  <DocxEditorContentControl.Header />
  <DocxEditorContentControl.Fields />
  <DocxEditorContentControl.Remove />
</DocxEditorContentControl>

The parts support the same class, as-child, and hidden controls. For content control behavior, see Content controls.

Page furniture

Mount a part only when your layout needs its interface:

PartInterface
DocxEditorPageNumberCurrent and total page count during scrolling
DocxEditorAuthorStyleReview style for one author
DocxEditorColorByChangeTypeTracked-change colors by change type
DocxEditorFontNoticeRendered families without a compatible face
DocxEditorDocumentOutlineStandalone heading list with caret navigation
DocxEditorHeaderFooterChromeHeader and footer editing controls
DocxEditorNotesChromeFootnote and endnote controls

For review colors, see Tracked changes.

Custom labels

Chrome resolves labels from the active locale catalog. Pass a translator only when you need label overrides.

<script setup lang="ts">
import { DocxEditorToolbar, useChromeTranslate } from '@docx-editor.dev/vue';

const overrides = new Map([
  ['contextMenu.cut', 'Cut text'],
  ['formattingBar.bold', 'Heavy'],
]);
const t = useChromeTranslate(overrides);
</script>

<template>
  <DocxEditorToolbar :t="t" />
</template>

Keys outside the map continue to use the locale catalog.

Custom colors

Set --doc-* tokens on a host scope. The tokens apply to chrome descendants.

.my-nav {
  --doc-surface: transparent;
  --doc-text: #fff;
  --doc-border: rgb(255 255 255 / 25%);
}

Do not style internal docx-* classes. Do not theme the document canvas. The canvas must show the document's saved colors.

Custom tracked changes and comments

Vue review chrome comes from @docx-editor.dev/pro/vue.

<script setup lang="ts">
import { h } from 'vue';
import { DocxEditorReview } from '@docx-editor.dev/pro/vue';
import ReviewFilters from './ReviewFilters.vue';

const reviewFurniture = h(ReviewFilters);
const commentsOnly = (item: { kind: string }) => item.kind === 'comment';
</script>

<template>
  <DocxEditorReview
    class-name="my-review"
    :filter="commentsOnly"
    :structural="false"
    :formatting="false"
    stack
    :gap="12"
    :furniture="reviewFurniture"
  />
</template>

Place the review rail inside the viewport. It then scrolls with the document.

Custom review cards

Override review card parts in place:

<DocxEditorReview class-name="my-review">
  <DocxEditorReview.List>
    <DocxEditorReview.Card class-name="my-card">
      <DocxEditorReview.Avatar class-name="my-avatar" />
      <DocxEditorReview.Author />
      <DocxEditorReview.Time hidden />
      <DocxEditorReview.Summary />
      <DocxEditorReview.Accept />
      <DocxEditorReview.Reject />
      <DocxEditorReview.Replies />
      <DocxEditorReview.Reply />
    </DocxEditorReview.Card>
    <DocxEditorReview.Empty>Nothing to review</DocxEditorReview.Empty>
  </DocxEditorReview.List>
</DocxEditorReview>

Use the item slot for unrelated card markup:

<DocxEditorReview>
  <DocxEditorReview.AddComment as-child>
    <MyAddCommentButton />
  </DocxEditorReview.AddComment>
  <DocxEditorReview.List>
    <template #item="{ item }">
      <MyReviewCard :item="item" />
    </template>
  </DocxEditorReview.List>
</DocxEditorReview>

Set :preset="false" to arrange all rail parts. Use useReview() for data without chrome. Use useReviewItem() inside a card.

Custom-node chrome

Import the custom-node components from the Vue Pro entry:

<script setup lang="ts">
import { CustomNodeChrome, CustomNodeContextMenu } from '@docx-editor.dev/pro/vue';
import type { ActivatedCustomNode } from '@docx-editor.dev/pro';

const openNode = (node: ActivatedCustomNode) => {
  // Open your popover or form.
};
</script>

<template>
  <CustomNodeChrome :on-node-click="openNode" />
  <DocxEditor.ContextMenu>
    <CustomNodeContextMenu :on-edit-node="openNode" />
  </DocxEditor.ContextMenu>
</template>

CustomNodeChrome applies each definition's chip color. It also dispatches click and hover activation. CustomNodeContextMenu adds information, edit, and remove rows before the packaged rows.

Full composition

This example places the main parts by name:

<script setup lang="ts">
import {
  DocxEditorContent,
  DocxEditorContextMenu,
  DocxEditorHeaderFooterChrome,
  DocxEditorHorizontalRuler,
  DocxEditorHyperLink,
  DocxEditorNavigation,
  DocxEditorNotesChrome,
  DocxEditorPageNumber,
  DocxEditorRoot,
  DocxEditorVerticalRuler,
  DocxEditorViewport,
} from '@docx-editor.dev/vue';
</script>

<template>
  <DocxEditorRoot :document="bytes" author="Jess Lin">
    <MyBrandHeader />
    <MyMenu />
    <MyToolbar />
    <DocxEditorHorizontalRuler />

    <div class="workspace">
      <DocxEditorNavigation />
      <DocxEditorViewport>
        <DocxEditorVerticalRuler />
        <DocxEditorHeaderFooterChrome />
        <DocxEditorNotesChrome />
        <DocxEditorContent />
        <DocxEditorHyperLink />
        <DocxEditorContextMenu />
      </DocxEditorViewport>
      <DocxEditorPageNumber />
    </div>
  </DocxEditorRoot>
</template>

An omitted part has no interface.

Layout constraints

Import the stylesheet once and give the editor container a height. Use a flex column when chrome shares that height. Packaged controls and the viewport apply their own .docx-editor scope. The wrapper in Primitives also makes editor styles and tokens available to custom controls.

Place the navigation pane and viewport in a row with position: relative. The pane uses that row as its positioning context.

Do not set z-index on the workspace row or viewport. Either value creates a stacking context that can put a fixed context menu below other chrome.

Keep the caret

A mousedown event that reaches the document moves the caret. Prevent the event on host chrome that must keep the document selection.

<div @mousedown="keepCaret">
  <!-- Add your toolbar here. -->
</div>

<script setup lang="ts">
function keepCaret(event: MouseEvent) {
  const tag = (event.target as HTMLElement | null)?.tagName;
  if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return;
  event.preventDefault();
}
</script>

Packaged chrome already applies this behavior.

Next steps