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
.docis unsupported — convert to DOCX.
Features
| Area | Behavior |
|---|---|
| Source | src: string | Blob | File |
| Auth | requestOptions for URL fetch, or app-side fetch→Blob |
| Chrome | Toolbar, zoom, download, optional thumbnail sidebar |
| Handle | scrollToPage, getViewportElement |
| WASM | Pins engineOptions.wasmUrl (default /vendor/docx_wasm_bg.wasm) via setWasmSource(Response) (main-thread parse) |
| Fidelity | Best-effort layout; complex Word features may differ from desktop Word |
Legacy .doc | Rejected with a clear unsupported message |
| i18n | Built-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-viewerFUI_PLUS_REGISTRY_TOKEN=xxx npx shadcn@latest add @f-ui-plus/docx-viewerFUI_PLUS_REGISTRY_TOKEN=xxx yarn dlx shadcn@latest add @f-ui-plus/docx-viewerFUI_PLUS_REGISTRY_TOKEN=xxx bun x shadcn@latest add @f-ui-plus/docx-viewerregistryDependencies: 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.
"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
└── viewportSecurity
| Threat | Default |
|---|---|
| DOCX active content | Engine path must not insert unsanitized HTML; treat files as untrusted |
| Unsafe download URLs | Safe URL policy for download actions |
| Auth tokens in URLs | Prefer requestOptions.headers or fetch→Blob; avoid putting secrets in query strings |
| Viewer-local upload | Not supported — supply src from your app |
| Missing WASM asset | Host docx_wasm_bg.wasm and set engineOptions.wasmUrl. f-ui fetches it and calls setWasmSource(Response) before parse |
Edge Cases & Errors
| Case | Behavior |
|---|---|
| Load / parse failure | Error surface with Retry for URL sources |
| CORS / credentials | Forward requestOptions, or fetch→Blob yourself |
Legacy .doc | Unsupported message (convert to DOCX) |
| Layout fidelity | Complex tables/floats may not match Word exactly |
| Object URL lifecycle | Owned blob URLs revoked on change/unmount |
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | Blob | File | (required) | Document source. |
requestOptions | { headers?; credentials? } | — | Forwarded for remote URL loads. |
engineOptions | { wasmUrl?: string } | wasmUrl: /vendor/docx_wasm_bg.wasm | Self-hosted WASM path. |
fileName | string | derived | Display / download name. |
defaultZoom | number | 1 | Initial zoom scale. |
defaultSidebarOpen | boolean | engine default | Thumbnail sidebar open state. |
showToolbar | boolean | true | Toolbar chrome. |
showDownload | boolean | true | Download control. |
toolbarActions | ReactNode | — | Extra toolbar actions. |
onDocumentLoadSuccess | ({ pageCount }) => void | — | Fired when pages are ready. |
onActivePageChange | (pageNumber) => void | — | 1-based active page. |
className | string | — | Root class. |
classNames | Partial<Record<DocxViewerSlot, string>> | — | Per-slot classes. |
t / locale | translator / string | — | Per-instance i18n overrides. |
Handle (ref): scrollToPage, getViewportElement.
Slots
| Slot | Applied to |
|---|---|
root | Outer viewer root |
toolbar | Toolbar strip |
status | Loading / error status |
viewport | Scrollable page viewport |
sidebar | Thumbnail sidebar |
Hook
useDocxViewer(options) returns { state, retry, setActivePage, setZoomScale, setSidebarOpen, download }. state includes the resolved file, loading/error flags, activePage, zoomScale, and sidebarOpen.