Composition
Build a custom React DOCX editor from primitives, compound parts, and hooks.
<DocxEditor> is a packaged arrangement of public components. Compose those components yourself when you need a different layout or your own controls.
Compose an editor
This example mounts a toolbar and the document in a bounded flex column:
import { DocxEditor } from '@docx-editor.dev/react';
import '@docx-editor.dev/core/styles/editor.css';
export function Editor({ bytes }: { bytes: Uint8Array }) {
return (
<div
className="docx-editor"
style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}
>
<DocxEditor.Root document={bytes}>
<DocxEditor.Toolbar />
<DocxEditor.Viewport style={{ flex: 1, minHeight: 0 }}>
<DocxEditor.Content />
<DocxEditor.HyperLink />
<DocxEditor.ContextMenu />
</DocxEditor.Viewport>
</DocxEditor.Root>
</div>
);
}| Requirement | Why |
|---|---|
DocxEditor.Root | Owns the editor instance. It renders no DOM element, so its children sit in your flex column. |
DocxEditor.Viewport | Supplies the scroll container. Content must be inside it. |
DocxEditor.Content | Mounts the painted, editable pages. |
| Editor stylesheet | Styles the viewport and packaged parts. Import it once. |
| Bounded height | Gives the viewport room. Use flex: 1 and min-height: 0 on the viewport and on any element between it and the container. |
.docx-editor wrapper | Makes editor styles and --doc-* tokens available to your own controls. |
If you use Pro review chrome, register its modules once on Root. For setup, see the Pro package documentation.
UI language and date input
Root reads UI strings from LocaleProvider and has no i18n prop. Set locale separately for regional date input:
import { DocxEditor, LocaleProvider, useChromeTranslate } from '@docx-editor.dev/react';
import { pl } from '@docx-editor.dev/i18n';
function LocalizedToolbar() {
const t = useChromeTranslate();
return <DocxEditor.Toolbar t={t} />;
}
<LocaleProvider i18n={pl}>
<DocxEditor.Root document={bytes} locale="pl-PL">
<LocalizedToolbar />
<DocxEditor.Viewport>
<DocxEditor.Content />
</DocxEditor.Viewport>
</DocxEditor.Root>
</LocaleProvider>;Pass a module-scope Map to useChromeTranslate(overrides) to rename labels. For catalogs, see Internationalization.
Reach the editor from a parent
Components inside Root call useDocxEditor(). A parent outside Root stores the instance from onReady:
import { useState } from 'react';
import { DocxEditor, type Editor } from '@docx-editor.dev/react';
export function Workspace({ bytes }: { bytes: Uint8Array }) {
const [editor, setEditor] = useState<Editor | null>(null);
return (
<>
<button disabled={!editor} onClick={() => void editor?.save()}>
Save
</button>
<DocxEditor.Root document={bytes} onReady={setEditor}>
{/* Toolbar, viewport, and content */}
</DocxEditor.Root>
</>
);
}onReady runs once per instance after Content attaches. A new document creates a new instance and calls it again.
Choose optional components
Mount only the parts that your layout needs. An omitted part renders no interface.
| Part | Purpose | Owner guide |
|---|---|---|
Toolbar, Menu | Formatting toolbar and menu bar | Toolbar, Menus |
HyperLink, ContentControl, Equation, ContextMenu | Popovers. Mount each one you use. | Customize popups |
HorizontalRuler, VerticalRuler | Margin and indent rulers. A drag commits one undo entry. | None |
Navigation | Headings, Find, and Bookmarks pane. Uses its t prop, else the translate of DocxEditor.Root, else the locale catalog. | Navigation |
Loading | Loading screen and open overlay | Custom loading screen |
PageSetupDialog, ParagraphDialog | Controlled dialogs with open and onClose | Dialogs |
PageNumber | Current and total page count | None |
HeaderFooterChrome, NotesChrome | Header, footer, and note editing controls | Headers and footers |
FontNotice | Families without a compatible face | Fonts |
OpenFailure | Why a file did not open | Open failures |
DocumentOutline | Standalone heading list | Navigation |
AuthorStyle, ColorByChangeType | Review colors | Review styling |
Window | Moves wrapped parts to a pop-out window | Pop-out windows |
DocxEditorReview (Pro) | Comment and tracked-change cards | Review cards |
CustomNodeChrome (Pro) | Custom-node markers and menu rows | Custom nodes |
Mount popovers
Root does not mount the four popovers that <DocxEditor> mounts. Add each one as a child of Viewport, or register it in the popups prop:
import { DocxEditor, definePopup } from '@docx-editor.dev/react';
const popups = {
hyperlink: definePopup(DocxEditor.HyperLink),
contentControl: definePopup(DocxEditor.ContentControl),
equation: definePopup(DocxEditor.Equation),
contextMenu: definePopup(DocxEditor.ContextMenu),
};
<DocxEditor.Root document={bytes} popups={popups}>
{/* Toolbar, viewport, and content */}
</DocxEditor.Root>;Without HyperLink, the link button, the Insert link row, and Ctrl+K open nothing. Without ContextMenu, a right-click opens the browser menu. Define custom popup components at module scope so they keep state across renders.
Custom loading screen
DocxEditor.Loading shows before the bytes arrive and while a large document opens. This example replaces the spinner and covers the previous document during a reopen:
<div style={{ position: 'relative' }}>
<DocxEditor.Viewport>
<DocxEditor.Content />
</DocxEditor.Viewport>
<DocxEditor.Loading overlay indicator={<MyLogoSpinner label="Opening document" />} />
</div>Children replace the whole first screen; indicator replaces only the spinner. Keep DocxEditor.Content mounted while the open completes. For the loading flow, see Loading and saving.
Override a component
A named child replaces the matching slot in the preset. hidden removes it, and asChild applies the part's behavior to your element:
import { Button } from '@/components/ui/button';
<DocxEditor.Toolbar>
<DocxEditor.Toolbar.Bold asChild>
<Button variant="ghost">Bold</Button>
</DocxEditor.Toolbar.Bold>
<DocxEditor.Toolbar.Highlight hidden />
</DocxEditor.Toolbar>;asChild passes handlers, disabled state, ARIA attributes, and active state to the child without a wrapper. A group whose slots are all hidden disappears with its separator.
Custom toolbar
Set preset={false} to render only the parts that you list, in JSX order:
<DocxEditor.Toolbar preset={false} className="my-toolbar">
<DocxEditor.Toolbar.Undo />
<DocxEditor.Toolbar.Redo />
<DocxEditor.Toolbar.Separator />
<DocxEditor.Toolbar.FontFamily />
<DocxEditor.Toolbar.Bold icon={MyBold} />
<DocxEditor.Toolbar.Zoom />
</DocxEditor.Toolbar>Each part reads enabled state, active state, and commands from the engine. For Toolbar.Group, Toolbar.Slot, and Toolbar.Action, see Customize the toolbar. For every slot and named part, see the chrome slot reference.
Control lifetime and focus
Root reads its props at construction. This table shows how each prop behaves after a change:
| Prop | Type | Behavior |
|---|---|---|
document | DocumentSource | Remounts the editor. |
imageDecodePort | ImageDecodePort | Remounts the editor. |
modules | readonly EditorModule[] | Ignored. Change the key to remount with new modules. |
fonts | FontConfiguration | FontConfigurationFragment | FontResolver | Replaces fonts in place. |
author | string | Applies to later comments and changes. |
locale | string | Updates date input and labels. Defaults to en-US. |
mode | 'edit' | 'view' | 'suggesting' | Updates in place. When omitted, w:trackRevisions can select suggesting. |
zoom, zoomMode | number, ZoomMode | 'auto' | Updates in place. |
colorMode | 'light' | 'dark' | 'system' | Updates the chrome theme, including dialogs and pop-out windows. |
translate | (key, params?) => string | Updates surface labels in place. |
tableInteractionLabel | (key) => string | Resolves table insertion and select-handle labels. |
popups | Popup map | Lists popups that Content renders. |
onReady, onChange, onFontError | Callbacks | Report readiness, document changes, and font errors. |
In-place updates keep edits, the caret, and undo history. For exact signatures, see the React API reference.
A mousedown that reaches the pages moves the caret. Prevent it on custom chrome, but not on form fields:
function keepCaret(event: React.MouseEvent) {
const tag = (event.target as HTMLElement).tagName;
if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return;
event.preventDefault();
}
<div onMouseDown={keepCaret}>{/* Your toolbar */}</div>;Packaged chrome already applies this rule.
Limits
Contentmust be insideViewport, and the viewport needs a bounded height.- Composed editors mount popovers explicitly or register them in
popups. - The
file.pageSetupandparagraph.dialogslots do not open a dialog. Theirexecute()returnsfalse. - Use
snapshot().isLoading, notisOpening, to mount content conditionally. Both flags can readfalsebriefly while the instance changes. - Place
NavigationinsideViewport, or beside it in a row withposition: relative. - Navigation overlays the page when less than 320 pixels remain beside it. Visible review space reduces that room. With review
overflow: 'scroll', only the marker strip counts. - Give the viewport's parent
position: relativeforPageNumber. An indicator inside the viewport renders into that parent. - With the context-menu preset enabled, unmatched children appear after the packaged rows.
- A
DocxEditorReviewroot callback cannot include sibling root parts. To add them, put the callback onDocxEditorReview.List. - Do not set
z-indexon the workspace row or viewport. It can trap the context menu under other chrome. - The
iconprop takes a React element, not a component function. - Theme chrome with
--doc-*tokens. Do not targetdocx-*classes, use!important, or theme the document canvas. See Appearance.