Composition
Build a Vue DOCX editor from root, viewport, content, chrome, and review primitives.
<DocxEditor> supplies one arrangement of public parts. Use the parts when you
need a different interface.
Primitives
Every editor needs these three components in this order:
<script setup lang="ts">
import { DocxEditorContent, DocxEditorRoot, DocxEditorViewport } from '@docx-editor.dev/vue';
</script>
<template>
<DocxEditorRoot :document="bytes">
<DocxEditorViewport>
<DocxEditorContent />
</DocxEditorViewport>
</DocxEditorRoot>
</template>DocxEditorRootowns the editor and provides it to descendants.DocxEditorViewportsupplies the scroll and page-layout container.DocxEditorContentmounts the painted, editable document surface.
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 samples document, fonts, translate, and imageDecodePort when it
creates the editor. An identity change rebuilds the editor. Changes to zoom
and zoomMode apply without a rebuild.
For full prop types, see the Vue API reference.
provideDocxEditor()
Call provideDocxEditor() when a parent layout owns 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.
Customization ladder
Use the first option that meets your needs:
- 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 use template order. Packaged parts still read command
state and disabled reasons from the editor.
<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>Add a host toolbar action
Use DocxEditorToolbar.Action for an action without a registry slot. Ask
useEditorCommand() 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
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. The sugar
host 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 reads 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.
<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.
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 only the furniture that your layout needs:
DocxEditorPageNumbershows the page count while the document scrolls.DocxEditorAuthorStyleassigns a tracked-change style to one author.DocxEditorColorByChangeTypecolors changes by type.DocxEditorFontNoticelists substituted fonts.DocxEditorDocumentOutlinerenders a standalone headings list.DocxEditorHeaderFooterChromesupplies header and footer controls.DocxEditorNotesChromesupplies footnote and endnote controls.
See Tracked changes for author colors.
Custom labels
Chrome reads 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. Register reviewModule
on the root.
<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 pitfalls
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
@docx-editor.dev/vue
Vue 3 adapter for the DOCX editor: the packaged root component, composition primitives, shared composables, and compound chrome, all from the package root.
Composables
The Vue API the packaged chrome is built on: subscribe to editor state, run commands, read the document outline, search, and drive page setup.