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>
  );
}
RequirementWhy
DocxEditor.RootOwns the editor instance. It renders no DOM element, so its children sit in your flex column.
DocxEditor.ViewportSupplies the scroll container. Content must be inside it.
DocxEditor.ContentMounts the painted, editable pages.
Editor stylesheetStyles the viewport and packaged parts. Import it once.
Bounded heightGives the viewport room. Use flex: 1 and min-height: 0 on the viewport and on any element between it and the container.
.docx-editor wrapperMakes 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.

PartPurposeOwner guide
Toolbar, MenuFormatting toolbar and menu barToolbar, Menus
HyperLink, ContentControl, Equation, ContextMenuPopovers. Mount each one you use.Customize popups
HorizontalRuler, VerticalRulerMargin and indent rulers. A drag commits one undo entry.None
NavigationHeadings, Find, and Bookmarks pane. Uses its t prop, else the translate of DocxEditor.Root, else the locale catalog.Navigation
LoadingLoading screen and open overlayCustom loading screen
PageSetupDialog, ParagraphDialogControlled dialogs with open and onCloseDialogs
PageNumberCurrent and total page countNone
HeaderFooterChrome, NotesChromeHeader, footer, and note editing controlsHeaders and footers
FontNoticeFamilies without a compatible faceFonts
OpenFailureWhy a file did not openOpen failures
DocumentOutlineStandalone heading listNavigation
AuthorStyle, ColorByChangeTypeReview colorsReview styling
WindowMoves wrapped parts to a pop-out windowPop-out windows
DocxEditorReview (Pro)Comment and tracked-change cardsReview cards
CustomNodeChrome (Pro)Custom-node markers and menu rowsCustom 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:

PropTypeBehavior
documentDocumentSourceRemounts the editor.
imageDecodePortImageDecodePortRemounts the editor.
modulesreadonly EditorModule[]Ignored. Change the key to remount with new modules.
fontsFontConfiguration | FontConfigurationFragment | FontResolverReplaces fonts in place.
authorstringApplies to later comments and changes.
localestringUpdates date input and labels. Defaults to en-US.
mode'edit' | 'view' | 'suggesting'Updates in place. When omitted, w:trackRevisions can select suggesting.
zoom, zoomModenumber, ZoomMode | 'auto'Updates in place.
colorMode'light' | 'dark' | 'system'Updates the chrome theme, including dialogs and pop-out windows.
translate(key, params?) => stringUpdates surface labels in place.
tableInteractionLabel(key) => stringResolves table insertion and select-handle labels.
popupsPopup mapLists popups that Content renders.
onReady, onChange, onFontErrorCallbacksReport 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

  • Content must be inside Viewport, and the viewport needs a bounded height.
  • Composed editors mount popovers explicitly or register them in popups.
  • The file.pageSetup and paragraph.dialog slots do not open a dialog. Their execute() returns false.
  • Use snapshot().isLoading, not isOpening, to mount content conditionally. Both flags can read false briefly while the instance changes.
  • Place Navigation inside Viewport, or beside it in a row with position: 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: relative for PageNumber. An indicator inside the viewport renders into that parent.
  • With the context-menu preset enabled, unmatched children appear after the packaged rows.
  • A DocxEditorReview root callback cannot include sibling root parts. To add them, put the callback on DocxEditorReview.List.
  • Do not set z-index on the workspace row or viewport. It can trap the context menu under other chrome.
  • The icon prop takes a React element, not a component function.
  • Theme chrome with --doc-* tokens. Do not target docx-* classes, use !important, or theme the document canvas. See Appearance.

See also