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
| Requirement | When required | Purpose |
|---|---|---|
DocxEditorRoot | Every composed editor | Owns and provides the editor instance |
DocxEditorViewport | Every composed editor | Supplies the scroll container and page-layout classes |
DocxEditorContent | Every composed editor | Mounts the painted document surface |
| Editor stylesheet | Every composed editor | Styles the viewport and packaged controls |
.docx-editor wrapper | Custom controls that use editor styles | Supplies scoped styles and tokens |
| Container height | Every composed editor | Gives the viewport room to render |
Remount :key | When you change modules | Loads the new modules |
| Positioned workspace row | When you use the navigation pane | Anchors the navigation pane |
| Pro review module | When you use Pro review chrome | Enables 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, andmoduleszoomandzoomModetranslateandtableInteractionLabelimageDecodePort
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:
- Set a class or a
--doc-*color token. - Replace a part's
iconprop. - Use
as-childto merge behavior into your element. - Override one compound part.
- Set
:preset="false"and arrange all parts. - 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.
Custom hyperlink popover
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:
| Part | Interface |
|---|---|
DocxEditorPageNumber | Current and total page count during scrolling |
DocxEditorAuthorStyle | Review style for one author |
DocxEditorColorByChangeType | Tracked-change colors by change type |
DocxEditorFontNotice | Rendered families without a compatible face |
DocxEditorDocumentOutline | Standalone heading list with caret navigation |
DocxEditorHeaderFooterChrome | Header and footer editing controls |
DocxEditorNotesChrome | Footnote 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.