Document Preview
Unified image, PDF, and DOCX preview overlay with Dialog or Sheet shells.
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/document-preview (pulls PDF Viewer and DOCX Viewer).
Document Preview is a controlled/uncontrolled overlay that routes by kind: image lightbox, PDF / DOCX near-fullscreen dialog (or explicit drawer), and a small unsupported dialog with a safe new-tab CTA when possible. Pair it with public File Upload via File Upload With Preview. PDF/DOCX demo fixtures are pinned from Extend UI commit bca60b9e204ef0db248466dfa44367ad4c23d404.
When To Use
- Open a preview overlay for image, PDF, or DOCX from a list, table, or upload row.
- You need Dialog vs Drawer placement without wiring viewers yourself.
- Replace the body with
previewRenderwhen you own a custom viewer. - Use embedded PDF Viewer / DOCX Viewer when the document stays on the page.
- For upload glue (controlled item, cancel, remove-closes), prefer the Plus recipe.
Features
| Area | Behavior |
|---|---|
| Open state | Controlled open / onOpenChange or uncontrolled defaultOpen |
| Kind routing | From kind, type, name, or src extension/MIME |
| Placement | auto: image→lightbox dialog; pdf/docx→near-fullscreen dialog; drawer→Sheet |
| Image chrome | Zoom, rotate, reset, download |
| PDF engine | Forwards engineOptions / requestOptions into PDF Viewer |
| Custom body | previewRender({ kind, src, name }) replaces the default engine |
| Unsupported | Compact dialog; Open in new tab when the URL is safe |
| i18n | Built-in en / zh-CN |
Self-Hosted WASM
PDF overlays need the same EmbedPDF WASM setup as PDF Viewer. Pass engineOptions={{ wasmUrl: "/vendor/pdfium.wasm" }} (this site’s path) or your own asset URL. Strict CSP must allow worker-src blob:. DOCX overlays need docx_wasm_bg.wasm — pass docxViewerProps={{ engineOptions: { wasmUrl: "/vendor/docx_wasm_bg.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/document-previewFUI_PLUS_REGISTRY_TOKEN=xxx npx shadcn@latest add @f-ui-plus/document-previewFUI_PLUS_REGISTRY_TOKEN=xxx yarn dlx shadcn@latest add @f-ui-plus/document-previewFUI_PLUS_REGISTRY_TOKEN=xxx bun x shadcn@latest add @f-ui-plus/document-previewregistryDependencies: document-internals, pdf-viewer, docx-viewer, shadcn dialog, sheet, button, tooltip, and fui-i18n. Runtime: lucide-react (plus viewer engine deps via the viewer packages).
Usage
import { DocumentPreview } from "@/components/f-ui/document-preview/document-preview";
export function Page() {
const [open, setOpen] = useState(false);
return (
<>
<button type="button" onClick={() => setOpen(true)}>
Preview
</button>
<DocumentPreview
open={open}
onOpenChange={setOpen}
src="/docs/sample.pdf"
name="sample.pdf"
engineOptions={{ wasmUrl: "/vendor/pdfium.wasm" }}
/>
</>
);
}Examples
Image / PDF / DOCX Switcher
Switch the source kind, then open the overlay. PDF and DOCX use the pinned local fixtures; image uses a tiny same-origin PNG.
"use client";
import { useState } from "react";
import { DocumentPreview } from "@/components/f-ui/document-preview/document-preview";
import { Button } from "@/components/ui/button";
type DemoKind = "image" | "pdf" | "docx";
const SOURCES: Record<
DemoKind,
{ src: string; name: string; type?: string }
> = {
image: {
src: "/demo-documents/demo.png",
name: "demo.png",
type: "image/png",
},
pdf: {
src: "/demo-documents/loan-application.pdf",
name: "loan-application.pdf",
type: "application/pdf",
},
docx: {
src: "/demo-documents/demo.docx",
name: "demo.docx",
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
},
};
/**
* PDF / DOCX fixtures pinned from Extend UI
* `bca60b9e204ef0db248466dfa44367ad4c23d404`.
*/
export function DocumentPreviewDemo() {
const [kind, setKind] = useState<DemoKind>("pdf");
const [open, setOpen] = useState(false);
const current = SOURCES[kind];
return (
<div className="space-y-3">
<div className="flex flex-wrap gap-2">
{(Object.keys(SOURCES) as DemoKind[]).map((key) => (
<Button
key={key}
onClick={() => setKind(key)}
type="button"
variant={kind === key ? "default" : "outline"}
>
{key.toUpperCase()}
</Button>
))}
<Button onClick={() => setOpen(true)} type="button">
Open preview
</Button>
</div>
<DocumentPreview
engineOptions={{ wasmUrl: "/vendor/pdfium.wasm" }}
name={current.name}
onOpenChange={setOpen}
open={open}
src={current.src}
type={current.type}
/>
</div>
);
}Drawer Placement
placement="drawer" mounts a Sheet shell and traps focus until dismissed.
"use client";
import { useState } from "react";
import { DocumentPreview } from "@/components/f-ui/document-preview/document-preview";
import { Button } from "@/components/ui/button";
/** Local fixture pinned from Extend UI `bca60b9e204ef0db248466dfa44367ad4c23d404`. */
const DEMO_PDF = "/demo-documents/loan-application.pdf";
export function DocumentPreviewDrawerDemo() {
const [open, setOpen] = useState(false);
return (
<div className="space-y-3">
<Button onClick={() => setOpen(true)} type="button">
Open drawer preview
</Button>
<DocumentPreview
engineOptions={{ wasmUrl: "/vendor/pdfium.wasm" }}
name="loan-application.pdf"
onOpenChange={setOpen}
open={open}
placement="drawer"
src={DEMO_PDF}
type="application/pdf"
/>
</div>
);
}Custom Body
previewRender replaces the default viewer body while keeping the overlay shell.
"use client";
import { useState } from "react";
import { DocumentPreview } from "@/components/f-ui/document-preview/document-preview";
import { Button } from "@/components/ui/button";
/** Local fixture pinned from Extend UI `bca60b9e204ef0db248466dfa44367ad4c23d404`. */
const DEMO_PDF = "/demo-documents/loan-application.pdf";
export function DocumentPreviewCustomBodyDemo() {
const [open, setOpen] = useState(false);
return (
<div className="space-y-3">
<Button onClick={() => setOpen(true)} type="button">
Open custom body
</Button>
<DocumentPreview
name="loan-application.pdf"
onOpenChange={setOpen}
open={open}
previewRender={({ kind, name }) => (
<div className="grid h-full place-items-center p-8 text-center">
<div className="space-y-2">
<p className="font-medium">Custom preview body</p>
<p className="text-muted-foreground text-sm">
kind: {kind} · name: {name ?? "—"}
</p>
<p className="text-muted-foreground text-sm">
Replace this slot with your own viewer or metadata panel.
</p>
</div>
</div>
)}
src={DEMO_PDF}
type="application/pdf"
/>
</div>
);
}Unsupported URL
Plain-text URLs resolve to unsupported and offer a safe new-tab action when allowed.
"use client";
import { useState } from "react";
import { DocumentPreview } from "@/components/f-ui/document-preview/document-preview";
import { Button } from "@/components/ui/button";
export function DocumentPreviewUnsupportedDemo() {
const [open, setOpen] = useState(false);
return (
<div className="space-y-3">
<Button onClick={() => setOpen(true)} type="button">
Open unsupported URL
</Button>
<DocumentPreview
name="notes.txt"
onOpenChange={setOpen}
open={open}
src="https://example.com/files/notes.txt"
type="text/plain"
/>
</div>
);
}Headless Usage
useDocumentPreview is the view-model: open state, resolved kind / shell, image transform helpers, download/new-tab policy, and prop bags for PDF Viewer / DOCX Viewer. Mount Dialog/Sheet yourself from those values.
open: no · kind: pdf · shell: dialog · dialogSize: document
canDownload: no · canOpenInNewTab: no
openInNewTabUrl: —
pdfViewerProps: ready · docxViewerProps: —
"use client";
import { useDocumentPreview } from "@/components/f-ui/document-preview/use-document-preview";
/** Local fixture pinned from Extend UI `bca60b9e204ef0db248466dfa44367ad4c23d404`. */
const DEMO_PDF = "/demo-documents/loan-application.pdf";
export function DocumentPreviewHeadlessDemo() {
const preview = useDocumentPreview({
src: DEMO_PDF,
name: "loan-application.pdf",
type: "application/pdf",
engineOptions: { wasmUrl: "/vendor/pdfium.wasm" },
defaultOpen: false,
});
return (
<div>
<p>
open: {preview.open ? "yes" : "no"} · kind: {preview.kind} · shell:{" "}
{preview.shell} · dialogSize: {preview.dialogSize}
</p>
<p>
canDownload: {preview.canDownload ? "yes" : "no"} · canOpenInNewTab:{" "}
{preview.canOpenInNewTab ? "yes" : "no"}
</p>
<p>openInNewTabUrl: {preview.openInNewTabUrl ?? "—"}</p>
<p>
pdfViewerProps: {preview.pdfViewerProps ? "ready" : "—"} ·
docxViewerProps: {preview.docxViewerProps ? "ready" : "—"}
</p>
<div>
<button onClick={() => preview.setOpen(true)} type="button">
Open
</button>{" "}
<button onClick={() => preview.close()} type="button">
Close
</button>{" "}
<button
disabled={!preview.canDownload}
onClick={() => preview.download()}
type="button"
>
Download
</button>
</div>
{preview.open ? (
<p>
Overlay is open in the view-model. Mount Dialog/Sheet + PdfViewer /
DocxViewer yourself from these props.
</p>
) : null}
</div>
);
}Composition
DocumentPreview
└── useDocumentPreview
├── DocumentPreviewDialog | DocumentPreviewSheet
└── body
├── ImagePreview (image)
├── PdfViewer (pdf)
├── DocxViewer (docx)
├── previewRender (custom)
└── DocumentPreviewStatus (loading / error / unsupported)Security
| Threat | Default |
|---|---|
| Unsafe URL actions | Only relative, http:, https:, and owned blob: URLs for download / new tab |
javascript: / data: | Rejected for preview navigation actions |
| Untrusted documents | Same boundaries as the embedded viewers |
| CSP | PDF path requires worker-src blob: and reachable WASM |
Edge Cases & Errors
| Case | Behavior |
|---|---|
| Unsupported kind | Compact dialog; new-tab CTA when URL is safe |
| Image load failure | Error status in the lightbox |
| PDF / DOCX load failure | Viewer error + retry inside the overlay |
| Rapid source changes | Latest wins; previous document unmounts |
| Object URLs | Revoked only when created by f-ui |
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open / defaultOpen / onOpenChange | controlled open API | uncontrolled closed | Overlay visibility. |
src | string | Blob | File | (required) | Preview source. |
name | string | — | Display name / alt hint. |
type | string | — | MIME hint for kind resolution. |
kind | "pdf" | "docx" | "image" | "unsupported" | resolved | Explicit kind override. |
requestOptions | document request options | — | Forwarded to viewers. |
engineOptions | { wasmUrl? } | — | PDF WASM URL. |
title | ReactNode | i18n default | Accessible overlay title. |
placement | "auto" | "dialog" | "drawer" | "auto" | Shell selection. |
showDownload | boolean | true | Download affordances where applicable. |
pdfViewerProps | omit source/engine/download | — | Extra PDF Viewer props. |
docxViewerProps | omit source/download | — | Extra DOCX Viewer props. |
previewRender | (ctx) => ReactNode | — | Custom body slot. |
className / classNames | string / slots | — | Root and slot classes. |
t / locale | translator / string | — | Per-instance i18n. |
Slots
| Slot | Applied to |
|---|---|
root | Dialog / Sheet root |
content | Overlay content surface |
header | Title row |
title | Title text |
body | Scrollport + embedded viewer/image root |
toolbar | Image lightbox toolbar |
status | Loading / error / unsupported status |
Hook
useDocumentPreview(options) returns open helpers (open, setOpen, close), routing (kind, shell, dialogSize), image transform controls, download/new-tab fields, and pdfViewerProps / docxViewerProps ready to spread onto the viewers.