f-ui
Components

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 previewRender when 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

AreaBehavior
Open stateControlled open / onOpenChange or uncontrolled defaultOpen
Kind routingFrom kind, type, name, or src extension/MIME
Placementauto: image→lightbox dialog; pdf/docx→near-fullscreen dialog; drawer→Sheet
Image chromeZoom, rotate, reset, download
PDF engineForwards engineOptions / requestOptions into PDF Viewer
Custom bodypreviewRender({ kind, src, name }) replaces the default engine
UnsupportedCompact dialog; Open in new tab when the URL is safe
i18nBuilt-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-preview
FUI_PLUS_REGISTRY_TOKEN=xxx npx shadcn@latest add @f-ui-plus/document-preview
FUI_PLUS_REGISTRY_TOKEN=xxx yarn dlx shadcn@latest add @f-ui-plus/document-preview
FUI_PLUS_REGISTRY_TOKEN=xxx bun x shadcn@latest add @f-ui-plus/document-preview

registryDependencies: 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

ThreatDefault
Unsafe URL actionsOnly relative, http:, https:, and owned blob: URLs for download / new tab
javascript: / data:Rejected for preview navigation actions
Untrusted documentsSame boundaries as the embedded viewers
CSPPDF path requires worker-src blob: and reachable WASM

Edge Cases & Errors

CaseBehavior
Unsupported kindCompact dialog; new-tab CTA when URL is safe
Image load failureError status in the lightbox
PDF / DOCX load failureViewer error + retry inside the overlay
Rapid source changesLatest wins; previous document unmounts
Object URLsRevoked only when created by f-ui

API Reference

Props

PropTypeDefaultDescription
open / defaultOpen / onOpenChangecontrolled open APIuncontrolled closedOverlay visibility.
srcstring | Blob | File(required)Preview source.
namestringDisplay name / alt hint.
typestringMIME hint for kind resolution.
kind"pdf" | "docx" | "image" | "unsupported"resolvedExplicit kind override.
requestOptionsdocument request optionsForwarded to viewers.
engineOptions{ wasmUrl? }PDF WASM URL.
titleReactNodei18n defaultAccessible overlay title.
placement"auto" | "dialog" | "drawer""auto"Shell selection.
showDownloadbooleantrueDownload affordances where applicable.
pdfViewerPropsomit source/engine/downloadExtra PDF Viewer props.
docxViewerPropsomit source/downloadExtra DOCX Viewer props.
previewRender(ctx) => ReactNodeCustom body slot.
className / classNamesstring / slotsRoot and slot classes.
t / localetranslator / stringPer-instance i18n.

Slots

SlotApplied to
rootDialog / Sheet root
contentOverlay content surface
headerTitle row
titleTitle text
bodyScrollport + embedded viewer/image root
toolbarImage lightbox toolbar
statusLoading / 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.

On this page