build69
build69  /  Components  /  Media & Files  /  DocumentCompare

DocumentCompare

v1.0.9Updated 20 Oct 2026

Two stored documents side by side in a dialog, and ONE decision (fb_56743998): the system-generated original next to the uploaded signed scan, each pane fetching its OWN file with its own loading skeleton, its own error and its own Retry, plus a footer slot for the decision button. It is a compare SHELL around DocumentViewer, not a second viewer: every pane is a DocumentViewerSurface, so the credentialed fetch-then-render, the PDF frame, the image wrapper, the explicit cannot-be-shown state, print and download all behave exactly as in the single-document viewer. DocumentCompareSurface is the dialog-less form for embedding and SSR.

Preview

Live · 12 stories
DefaultOpen ↗
▸Why this is recommendeddocumentcompareside-by-sidepdf+9

The default, out-of-the-box DocumentCompare.

Recommended for+14
  • ✓Compare the original document with the uploaded scan and confirm they are the same
  • ✓Check a signed copy against what the system generated, by eye, then approve
  • ✓Show two versions or revisions of a stored file next to each other
  • ✓Any approval step where a human has to look at two files before deciding
  • ✓A review dialog where one of the two files may be missing or may fail to load
Tags+6
documentcompareside-by-sidepdfscansigneddiffreviewapprovaldialogattachmenttwo-updefault
Features+4
  • Each pane owns its request: one pane 404-ing leaves the other readable, with its own message, HTTP status and Retry — and retrying one pane re-requests only that pane
  • A per-pane error BOUNDARY as well, so a pane that throws while rendering (a host node from a bad row) cannot blank the other document
  • Reuses DocumentViewerSurface verbatim for the rendering, so PDFs, images, office types, print and download behave identically to the single-document viewer and cannot drift from it
  • Phone answer: below md the panes become ONE pane at a time behind a radiogroup switch (arrow keys, one tab stop); above md they sit side by side. Both panes stay MOUNTED either way, so a switch never re-fetches a multi-megabyte scan and a pane that already failed keeps its error
  • Source allow-list BEFORE anything is fetched: relative or http(s) only by default, so a javascript:, data: or blob: address stored in a row is refused and never requested — and the refused address is never echoed back into the page
  • A cross-origin refusal is EXPLAINED: the pane names the site that declined instead of surfacing the browser bare Failed to fetch, and it is not blamed on CORS when the request did produce an HTTP status
  • A pane with no file yet is a different screen from a pane whose address was refused, and both are different from a fetch that failed
  • File names are rendered as text with control and direction-override characters stripped, so a scan cannot be named to read as a different file; a blank name falls back to a caption
  • Layouts: auto (switch below md), columns (always side by side), rows (always stacked). Two panes is the designed case; three or more are laid out the same way with no extra code
  • Object-URL hygiene inherited per pane: both blob URLs are revoked and both in-flight requests aborted when the dialog closes
  • i18n: labels for the shell (DocumentCompareLabels) and viewerLabels for the viewers inside the panes, so one translation pass covers the whole dialog. English defaults, {origin} template
Not recommended for−2
  • ✕Comparing TEXT line by line (use DiffViewer)
  • ✕Opening a single document (use DocumentViewer)
  • ✕Browsing many files (use FileBrowser)
  • ✕Editing or annotating an image (use ImageAnnotator)
  • ✕Automatic matching of two documents: this component shows them, a human decides

Install

Ask your agent, or call the MCP tool directly. The resolved bundle is written into your project.

get_registry_item({ name: "DocumentCompare" })

Usage

import { DocumentCompare } from '@/components/media/document-compare';

<DocumentCompare
  open={open}
  onOpenChange={setOpen}
  title="Check the signed delivery order"
  panes={[
    { id: 'original', title: 'System original', fileName: 'DO-2569-0184.pdf',
      src: '/api/delivery-orders/184/pdf' },
    { id: 'signed', title: 'Signed scan', fileName: scan.fileName,
      src: '/api/attachments/' + scan.id + '/content' },
  ]}
  footer={<Button tone="success" onClick={confirm}>These are the same document</Button>}
/>;

Tick variants in the left menu — each adds a tab with that variant’s real story source, the same code the preview above renders.

Props

PropTypeRequired
open

Dialog visibility (controlled). DocumentCompareSurface does not take it

booleanyes
onOpenChange

Open-state change callback. Closing unmounts both panes, which aborts both requests and revokes both blob URLs

(open: boolean) => voidyes
panes

The documents: { id?, title?, badge?, description?, src?, srcDoc?, fileName, mimeType?, fetchInit? }. Two is the designed case. A duplicate or blank id is resolved rather than silently breaking the keys and the switch

DocumentComparePane[]yes
layout

auto (default): one pane at a time behind a switch below md, side by side above it. columns: always side by side, no switch. rows: always stacked, no switch

'auto' | 'columns' | 'rows'—
view

Controlled narrow-screen pane, by id. A view naming a pane that no longer exists still shows the first one

string—
defaultView

Which pane a narrow screen opens on. Default: the first

string—
onViewChange

Fires when the narrow-screen switch is used

(id: string) => void—
footer

The decision slot: the button the screen exists for (confirm same document). Sits at the start of the dialog footer, opposite Close

React.ReactNode—
title

Dialog headline. Defaults to the title label

React.ReactNode—
description

Line under the headline. false removes it

React.ReactNode | false—
srcAllowed

May this document URL be fetched? Default isSafeDocumentSrc: relative or http(s) only. Widen it deliberately for blob: previews the host created itself

(src: string) => boolean—
pageOrigin

The origin this page is served from, used only to explain a cross-origin refusal. Defaults to the browser own origin, read when a failure renders (never during the first render, so the server and the client agree)

string—
paneClassName

Height of one pane. Default h-[60dvh], which fits a dialog; an embedded surface usually wants its own

string—
labels

Override every built-in string of the shell (captions fallback, switch label, missing / blocked / cross-origin / crashed panels, retry, close) for i18n. English defaults exported as DEFAULT_DOCUMENT_COMPARE_LABELS

Partial<DocumentCompareLabels>—
viewerLabels

Strings of the viewer inside each pane (print, download, its own error headline, the loading name), so one translation pass covers the whole dialog

Partial<DocumentViewerLabels>—

Related