f-ui
Components

DOCX Viewer

Extend-backed DOCX reader with zoom, thumbnails, and authenticated Blob loading recipes.

Plus Registry

This component ships from registry.plus.json, not the public registry.json. Set up @f-ui-plus on the Installation page, then install @f-ui-plus/docx-viewer.

DOCX Viewer renders modern Word (.docx) documents from a URL, Blob, or File with zoom, page thumbnails, and download. It powers DOCX overlays inside Document Preview. Demo DOCX files are local same-origin fixtures pinned from Extend UI commit bca60b9e204ef0db248466dfa44367ad4c23d404.

When To Use

  • Embed a DOCX for reading inside a page or custom shell.
  • You need zoom, thumbnail navigation, and download without Office Online.
  • Authenticated downloads should become a Blob/File before render (see Examples).
  • Prefer Document Preview for modal/drawer preview.
  • Use PDF Viewer for PDFs. Legacy .doc is unsupported — convert to DOCX.

Features

AreaBehavior
Sourcesrc: string | Blob | File
AuthrequestOptions for URL fetch, or app-side fetch→Blob
ChromeToolbar, zoom, download, optional thumbnail sidebar
HandlescrollToPage, getViewportElement
WASMPins engineOptions.wasmUrl (default /vendor/docx_wasm_bg.wasm) via setWasmSource(Response) (main-thread parse)
FidelityBest-effort layout; complex Word features may differ from desktop Word
Legacy .docRejected with a clear unsupported message
i18nBuilt-in en / zh-CN via useDocxViewerI18n

Self-Hosted WASM

Copy docx_wasm_bg.wasm from node_modules/@extend-ai/react-docx/dist/ into your public folder (this docs site uses public/vendor/ via predev / prebuild). Pass engineOptions={{ wasmUrl: "/vendor/docx_wasm_bg.wasm" }} (or your CDN path). The viewer must run as a Client Component — it uses browser APIs and WASM.

Installing

Configure @f-ui-plus and FUI_PLUS_REGISTRY_TOKEN as in Installation — Plus Registry.

FUI_PLUS_REGISTRY_TOKEN=xxx pnpm dlx shadcn@latest add @f-ui-plus/docx-viewer
FUI_PLUS_REGISTRY_TOKEN=xxx npx shadcn@latest add @f-ui-plus/docx-viewer
FUI_PLUS_REGISTRY_TOKEN=xxx yarn dlx shadcn@latest add @f-ui-plus/docx-viewer
FUI_PLUS_REGISTRY_TOKEN=xxx bun x shadcn@latest add @f-ui-plus/docx-viewer

registryDependencies: document-internals, shadcn button, select, separator, tooltip, and fui-i18n. Runtime: @extend-ai/react-docx, @tanstack/react-virtual, lucide-react.

Host WASM Asset

mkdir -p public/vendor
cp node_modules/@extend-ai/react-docx/dist/docx_wasm_bg.wasm public/vendor/

On Next.js App Router, keep the viewer behind a Client Component boundary ("use client" is already on the shipped files). Import it from a client page/component — do not render it directly from a Server Component without a client child.

Usage

"use client";

import { DocxViewer } from "@/components/f-ui/docx-viewer/docx-viewer";

export function Page() {
  return (
    <div className="h-[560px]">
      <DocxViewer
        src="/docs/sample.docx"
        fileName="sample.docx"
        engineOptions={{ wasmUrl: "/vendor/docx_wasm_bg.wasm" }}
      />
    </div>
  );
}

Examples

Default Embed

Loads the pinned local demo DOCX with toolbar and thumbnails.

Scroll to load demo…
"use client";

import { DocxViewer } from "@/components/f-ui/docx-viewer/docx-viewer";
import { DocumentDemoWhenVisible } from "@/demos/document-demo-when-visible";

/** Local fixture pinned from Extend UI `bca60b9e204ef0db248466dfa44367ad4c23d404`. */
const DEMO_DOCX = "/demo-documents/demo.docx";

export function DocxViewerDemo() {
  return (
    <DocumentDemoWhenVisible>
      <div className="h-[560px] overflow-hidden rounded-xl border">
        <DocxViewer
          fileName="demo.docx"
          src={DEMO_DOCX}
          engineOptions={{ wasmUrl: "/vendor/docx_wasm_bg.wasm" }}
        />
      </div>
    </DocumentDemoWhenVisible>
  );
}

Authenticated Blob Loading

Fetch with auth headers (or cookies), then pass the resulting Blob as src. The demo uses the same-origin fixture so it stays offline-friendly; uncomment the Authorization header in your app.

Fetching document as Blob…

"use client";

import { useEffect, useState } from "react";

import { DocxViewer } from "@/components/f-ui/docx-viewer/docx-viewer";
import { Button } from "@/components/ui/button";

/**
 * Recipe: fetch with auth headers, then pass a Blob/File as `src`.
 * Demo uses the same-origin Extend-pinned fixture so it stays offline-friendly.
 */
const DEMO_DOCX = "/demo-documents/demo.docx";

export function DocxViewerAuthenticatedBlobDemo() {
  const [blob, setBlob] = useState<Blob | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  const load = async () => {
    setLoading(true);
    setError(null);
    try {
      // In production, pass Authorization / credentials here.
      const response = await fetch(DEMO_DOCX, {
        headers: {
          // "Authorization": "Bearer <token>",
        },
      });
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      setBlob(await response.blob());
    } catch (err) {
      setBlob(null);
      setError(err instanceof Error ? err.message : "Failed to load");
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    void load();
  }, []);

  return (
    <div className="space-y-3">
      <div className="flex flex-wrap items-center gap-2">
        <Button
          disabled={loading}
          onClick={() => void load()}
          type="button"
          variant="outline"
        >
          {loading ? "Loading…" : "Reload as Blob"}
        </Button>
        {error ? (
          <p className="text-destructive text-sm">{error}</p>
        ) : null}
      </div>
      {blob ? (
        <div className="h-[560px] overflow-hidden rounded-xl border">
          <DocxViewer
            fileName="demo.docx"
            src={blob}
            engineOptions={{ wasmUrl: "/vendor/docx_wasm_bg.wasm" }}
          />
        </div>
      ) : (
        <p className="text-muted-foreground text-sm">
          Fetching document as Blob…
        </p>
      )}
    </div>
  );
}

Headless Usage

useDocxViewer owns fetch/abort, chrome state (page, zoom, sidebar), retry, and download. Drive native controls from the return value; stock toolbar parts are optional.

displayFileName: demo.docx

loading: yes

loadError:

legacyDoc: no

activePage: 1 · zoom: 1 · sidebar: closed

file:

"use client";

import { useDocxViewer } from "@/components/f-ui/docx-viewer/use-docx-viewer";

/** Local fixture pinned from Extend UI `bca60b9e204ef0db248466dfa44367ad4c23d404`. */
const DEMO_DOCX = "/demo-documents/demo.docx";

export function DocxViewerHeadlessDemo() {
  const {
    state,
    retry,
    setActivePage,
    setZoomScale,
    setSidebarOpen,
    download,
  } = useDocxViewer({ src: DEMO_DOCX, fileName: "demo.docx" });

  return (
    <div>
      <p>displayFileName: {state.displayFileName}</p>
      <p>loading: {state.isLoadingSource ? "yes" : "no"}</p>
      <p>
        loadError:{" "}
        {state.loadError ? state.loadError.message : "—"}
      </p>
      <p>legacyDoc: {state.isLegacyDoc ? "yes" : "no"}</p>
      <p>
        activePage: {state.activePage} · zoom: {state.zoomScale} · sidebar:{" "}
        {state.sidebarOpen ? "open" : "closed"}
      </p>
      <p>
        file:{" "}
        {state.file
          ? `${state.file.name} (${state.file.size} bytes)`
          : "—"}
      </p>
      <div>
        <button
          onClick={() => setActivePage(Math.max(1, state.activePage - 1))}
          type="button"
        >
          Prev page
        </button>{" "}
        <button
          onClick={() => setActivePage(state.activePage + 1)}
          type="button"
        >
          Next page
        </button>{" "}
        <button onClick={() => setZoomScale((z) => z + 0.25)} type="button">
          Zoom in
        </button>{" "}
        <button
          onClick={() => setSidebarOpen((open) => !open)}
          type="button"
        >
          Toggle sidebar
        </button>{" "}
        <button onClick={() => retry()} type="button">
          Retry
        </button>{" "}
        <button
          onClick={() => {
            void download();
          }}
          type="button"
        >
          Download
        </button>
      </div>
    </div>
  );
}

Composition

DocxViewer
├── useDocxViewer (source + chrome state)
└── ExtendDocxEngine (vendored)
    ├── DocxViewerToolbar
    ├── DocxViewerStatus
    ├── DocxThumbnailSidebar
    └── viewport

Security

ThreatDefault
DOCX active contentEngine path must not insert unsanitized HTML; treat files as untrusted
Unsafe download URLsSafe URL policy for download actions
Auth tokens in URLsPrefer requestOptions.headers or fetch→Blob; avoid putting secrets in query strings
Viewer-local uploadNot supported — supply src from your app
Missing WASM assetHost docx_wasm_bg.wasm and set engineOptions.wasmUrl. f-ui fetches it and calls setWasmSource(Response) before parse

Edge Cases & Errors

CaseBehavior
Load / parse failureError surface with Retry for URL sources
CORS / credentialsForward requestOptions, or fetch→Blob yourself
Legacy .docUnsupported message (convert to DOCX)
Layout fidelityComplex tables/floats may not match Word exactly
Object URL lifecycleOwned blob URLs revoked on change/unmount

API Reference

Props

PropTypeDefaultDescription
srcstring | Blob | File(required)Document source.
requestOptions{ headers?; credentials? }Forwarded for remote URL loads.
engineOptions{ wasmUrl?: string }wasmUrl: /vendor/docx_wasm_bg.wasmSelf-hosted WASM path.
fileNamestringderivedDisplay / download name.
defaultZoomnumber1Initial zoom scale.
defaultSidebarOpenbooleanengine defaultThumbnail sidebar open state.
showToolbarbooleantrueToolbar chrome.
showDownloadbooleantrueDownload control.
toolbarActionsReactNodeExtra toolbar actions.
onDocumentLoadSuccess({ pageCount }) => voidFired when pages are ready.
onActivePageChange(pageNumber) => void1-based active page.
classNamestringRoot class.
classNamesPartial<Record<DocxViewerSlot, string>>Per-slot classes.
t / localetranslator / stringPer-instance i18n overrides.

Handle (ref): scrollToPage, getViewportElement.

Slots

SlotApplied to
rootOuter viewer root
toolbarToolbar strip
statusLoading / error status
viewportScrollable page viewport
sidebarThumbnail sidebar

Hook

useDocxViewer(options) returns { state, retry, setActivePage, setZoomScale, setSidebarOpen, download }. state includes the resolved file, loading/error flags, activePage, zoomScale, and sidebarOpen.

On this page