docduck.ioGive your words wings.
Back to your documents
FOR YOUR WEBSITE

A real document editor.
Right where people need it.

Let people work on Word documents inside your website. Your application keeps control of users, storage and saving.

Embed the public DocDuck editor in your website. DOCX files up to 20 MB are supported. PDF and older .doc files are not editable.

Read the embedding guide

Try the embedded editor

This demonstration keeps document data in the current session.

Interactive demo

Connecting the demonstration editor…

THE INTEGRATION

Your document. Your storage.

The example connects to https://docduck.io. Its origin must differ from your website origin so the editor sandbox stays in place. Set a height on the editor container so the frame has room to display. Keep tokens and account credentials in your own application.

For a strict Content Security Policy, replace YOUR_RESPONSE_CSP_NONCE with a fresh, unpredictable nonce generated for each response and use it on both script tags. Allow the DocDuck editor origin in frame-src. If your policy uses 'strict-dynamic', add the nonce to the SDK script tag as shown instead of relying on its hostname in script-src.

The SDK checks the iframe origin and source on every message. The editor accepts only its explicitly named parent origin. Use HTTPS, or HTTP on localhost while developing.

change, saving and saved describe the editor session. A saved event means bytes are prepared in memory. It does not mean your backend has saved a file. Call export() and persist the returned bytes yourself.

Export the current document before replacing it after edits. Call destroy() when removing the editor to close its session and remove listeners.

<script nonce="YOUR_RESPONSE_CSP_NONCE" src="https://docduck.io/embed.js"></script>
<button id="new-docx" type="button" disabled>New blank document</button>
<input id="docx-file" type="file" accept=".docx" disabled>
<button id="export-docx" type="button" disabled>Export DOCX</button>
<p id="editor-status" role="status">Connecting…</p>
<div id="document-editor" style="height: 850px"></div>

<script nonce="YOUR_RESPONSE_CSP_NONCE" type="module">
  const newButton = document.querySelector("#new-docx");
  const fileInput = document.querySelector("#docx-file");
  const exportButton = document.querySelector("#export-docx");
  const status = document.querySelector("#editor-status");
  const editor = window.DocDuck.mount({
    element: "#document-editor",
    url: "https://docduck.io/embed"
  });
  let connected = false;
  let hasDocument = false;
  let reconnecting = false;
  let sessionGeneration = 0;
  const updateControls = () => {
    newButton.disabled = !connected;
    fileInput.disabled = !connected;
    exportButton.disabled = !connected || !hasDocument;
  };

  editor.on("ready", () => {
    connected = true;
    updateControls();
    status.textContent = reconnecting
      ? "Reconnected. Reopen the latest DOCX saved by your website, or create a new document."
      : "Connected. Create a blank document or open a Word file.";
    reconnecting = false;
  });
  editor.on("change", () => {
    status.textContent = "Changes are ready to export.";
  });
  editor.on("error", (error) => {
    if (error.code === "RELOADED") {
      sessionGeneration += 1;
      reconnecting = true;
      connected = false;
      hasDocument = false;
      updateControls();
      status.textContent = "The editor reloaded. Reopen the latest DOCX saved by your website. Unsaved edits were lost.";
      return;
    }
    status.textContent = error.message;
  });

  try {
    await editor.ready;
    connected = true;
    updateControls();
    if (!reconnecting) status.textContent = "Create a blank document or choose a Word file.";
  } catch (error) {
    status.textContent = error.message;
  }

  newButton.addEventListener("click", async () => {
    const generation = sessionGeneration;
    try {
      await editor.createBlank();
      if (generation !== sessionGeneration) return;
      hasDocument = true;
      updateControls();
      status.textContent = "Edit the document, then export your changes.";
    } catch (error) {
      if (generation !== sessionGeneration) return;
      status.textContent = error.message;
    }
  });

  fileInput.addEventListener("change", async () => {
    const file = fileInput.files[0];
    if (!file) return;
    const generation = sessionGeneration;
    try {
      const bytes = await file.arrayBuffer();
      if (generation !== sessionGeneration) return;
      await editor.load({ bytes, fileName: file.name });
      if (generation !== sessionGeneration) return;
      hasDocument = true;
      updateControls();
      status.textContent = "Edit the document, then export your changes.";
    } catch (error) {
      if (generation !== sessionGeneration) return;
      status.textContent = error.message;
    } finally {
      fileInput.value = "";
    }
  });

  exportButton.addEventListener("click", async () => {
    const generation = sessionGeneration;
    try {
      const result = await editor.export();
      if (generation !== sessionGeneration) return;
      const url = URL.createObjectURL(new Blob([result.bytes], {
        type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
      }));
      const link = document.createElement("a");
      link.href = url;
      link.download = result.fileName;
      link.click();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
      status.textContent = "Downloaded. Send these bytes to your own storage to keep them.";
    } catch (error) {
      if (generation !== sessionGeneration) return;
      status.textContent = error.message;
    }
  });

  // Call editor.destroy() when removing the editor from your page.
</script>