TutorialMay 20, 20264 min read

Nuxt DOCX editor: edit Word documents in a Nuxt app

Add a DOCX editor to a Nuxt 3 or 4 app with @docx-editor.dev/vue. Configure Nuxt, keep the editor client-only, and load and save .docx files.

To edit Word documents in a Nuxt app, use @docx-editor.dev/vue, the Vue 3 adapter. Nuxt needs two pieces of configuration: keep the editor out of server-side rendering, and prebundle the packages for the Vite development server.

The editor parses Word OOXML in the browser. Documents leave the client only if your app uploads them.

Live demo

This demo uses the component from this guide. Edit the sample, use the toolbar, or open a .docx file:

Install

Install the adapter and its engine peer:

npm install @docx-editor.dev/vue @docx-editor.dev/core

@docx-editor.dev/core and Vue 3 are peer dependencies. The adapter bundles the string catalog, so @docx-editor.dev/i18n is not a separate install.

Configure Nuxt

Load the stylesheet once, and prebundle the adapter and engine for the Vite development server:

// nuxt.config.ts
export default defineNuxtConfig({
  css: ['@docx-editor.dev/vue/styles.css'],
  vite: {
    optimizeDeps: {
      include: ['@docx-editor.dev/core', '@docx-editor.dev/vue'],
    },
  },
});

Without the optimizeDeps entries, the development server re-optimizes these packages on first load and reloads the page mid-mount.

Add a client-only component

The editor measures DOM geometry when it mounts, and a server has no DOM. Rendering it during SSR throws window is not defined.

Name the component with a .client.vue suffix. Nuxt excludes that component from server-side rendering:

<!-- components/DocumentEditor.client.vue -->
<script setup lang="ts">
import { DocxEditor } from '@docx-editor.dev/vue';
</script>
 
<template>
  <div class="editor-host">
    <DocxEditor document="blank" />
  </div>
</template>
 
<style scoped>
.editor-host {
  height: 100vh;
}
</style>

<DocxEditor> fills its parent, so set a nonzero height on the parent.

Then use the component in a page. Nuxt auto-imports components from components/, so there is no import line:

<!-- pages/editor.vue -->
<template>
  <DocumentEditor />
</template>

Load a .docx file

The document prop accepts an ArrayBuffer, a Uint8Array, a DocumentHandle, or the string 'blank'.

To load a document from a URL, fetch the bytes after mount:

<!-- components/RemoteDocument.client.vue -->
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { DocxEditor } from '@docx-editor.dev/vue';
 
const props = defineProps<{ url: string }>();
const bytes = ref<Uint8Array>();
 
onMounted(async () => {
  const response = await fetch(props.url);
  bytes.value = new Uint8Array(await response.arrayBuffer());
});
</script>
 
<template>
  <DocxEditor :document="bytes" />
</template>

Fetch in onMounted rather than with useFetch. The component renders only in the browser, so a Nuxt data fetch adds no benefit here.

To let a user open their own document, read the picked file as an ArrayBuffer:

<!-- components/DocumentPicker.client.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { DocxEditor } from '@docx-editor.dev/vue';
 
const bytes = ref<Uint8Array>();
 
async function onFile(event: Event) {
  const file = (event.target as HTMLInputElement).files?.[0];
  if (file) bytes.value = new Uint8Array(await file.arrayBuffer());
}
</script>
 
<template>
  <input type="file" accept=".docx" @change="onFile" />
  <DocxEditor v-if="bytes" :document="bytes" />
</template>

Save a .docx file

Call save() on the component ref. It returns Promise<ArrayBuffer | null>, and returns null when no document is open.

To store the document on your own server, send the buffer to a Nitro route:

<!-- components/SavingEditor.client.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/vue';
 
defineProps<{ bytes: Uint8Array }>();
 
const editorRef = ref<DocxEditorRef | null>(null);
 
async function save() {
  const buffer = await editorRef.value?.save();
  if (!buffer) return;
  await $fetch('/api/documents/42', { method: 'PUT', body: buffer });
}
</script>
 
<template>
  <button @click="save">Save</button>
  <DocxEditor ref="editorRef" :document="bytes" />
</template>

To download the document instead, build a blob from the same buffer:

<!-- components/DownloadingEditor.client.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/vue';
 
const DOCX_MIME =
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
 
const editorRef = ref<DocxEditorRef | null>(null);
 
async function download() {
  const buffer = await editorRef.value?.save();
  if (!buffer) return;
 
  const blob = new Blob([buffer], { type: DOCX_MIME });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = 'edited.docx';
  link.click();
  URL.revokeObjectURL(url);
}
</script>
 
<template>
  <button @click="download">Download .docx</button>
  <DocxEditor ref="editorRef" document="blank" />
</template>

save() returns the same OOXML format that the editor reads. It serializes modeled XML, retains unmodeled XML structurally, and preserves binary package payloads byte for byte.

Add tracked changes and comments

Tracked changes and comments come from @docx-editor.dev/pro. That package is commercially licensed: free to evaluate, and production use needs an agreement.

npm install @docx-editor.dev/pro

Register reviewModule through modules, and render the review rail inside the viewport. The review rail requires the composed parts instead of the packaged component:

<!-- components/ReviewEditor.client.vue -->
<script setup lang="ts">
import {
  DocxEditorContent,
  DocxEditorRoot,
  DocxEditorToolbar,
  DocxEditorViewport,
} from '@docx-editor.dev/vue';
import { DocxEditorReview, reviewModule } from '@docx-editor.dev/pro/vue';
 
defineProps<{ bytes: Uint8Array }>();
 
const modules = [reviewModule()];
</script>
 
<template>
  <DocxEditorRoot
    :document="bytes"
    :modules="modules"
    author="Jess Lin"
    mode="suggesting"
  >
    <DocxEditorToolbar />
    <DocxEditorViewport>
      <DocxEditorContent />
      <DocxEditorReview />
    </DocxEditorViewport>
  </DocxEditorRoot>
</template>

In suggesting mode, the editor wraps each edit in revision markup and attributes it to author. A reviewer accepts or rejects those revisions from the rail.

For more information, see Tracked changes and Comments.

Localize the interface

Pass a catalog through the i18n prop. The per-locale subpath keeps the other catalogs out of the chunk:

<!-- components/PolishEditor.client.vue -->
<script setup lang="ts">
import { DocxEditor } from '@docx-editor.dev/vue';
import pl from '@docx-editor.dev/i18n/pl';
 
defineProps<{ bytes: Uint8Array }>();
</script>
 
<template>
  <DocxEditor :document="bytes" :i18n="pl" locale="pl" />
</template>

@docx-editor.dev/i18n includes English, German, French, Hebrew, Hindi, Indonesian, Polish, Brazilian Portuguese, Turkish, and Simplified Chinese.

This demo runs with the Polish locale:

License

@docx-editor.dev/vue is open source under Apache 2.0. You can use it in personal and commercial projects, modify the source, and redistribute it. There are no usage limits and no watermarks.

@docx-editor.dev/pro and @docx-editor.dev/editor-api are commercially licensed, and both are free to evaluate.

The packages/vue/ directory contains the source in eigenpal/docx-editor on GitHub.

Where to go next