React integration
@broadpaper/react wraps the framework-neutral editor and preview. Props mirror ReportDesignerOptions; the ref exposes the imperative API.
pnpm add @broadpaper/core @broadpaper/blocks @broadpaper/renderer @broadpaper/editor @broadpaper/reactReact 18 or 19, as a peer dependency — the package uses whichever one your application already has rather than bringing its own.
<ReportDesigner />
Complete, and it compiles — the Quick start defines dataSources, sampleData and brand in the files this imports:
import { useRef, useState } from "react";
import { ReportDesigner, type ReportDesignerInstance } from "@broadpaper/react";
import type { Issue, ReportTemplate } from "@broadpaper/core";
import { dataSources } from "./data-sources";
import { sampleData } from "./sample-data";
import { brand, whiteLabel } from "./themes";
export function Editor({ initialTemplate }: { initialTemplate?: ReportTemplate }) {
const ref = useRef<ReportDesignerInstance>(null);
const [template, setTemplate] = useState<ReportTemplate | undefined>(initialTemplate);
const [issues, setIssues] = useState<Issue[]>([]);
return (
<div style={{ height: "100vh" }}>
<button type="button" onClick={() => ref.current?.setMode("preview")}>
Preview ({issues.length} checks)
</button>
<ReportDesigner
ref={ref}
template={template}
dataSources={dataSources}
sampleData={sampleData}
theme={brand}
themes={[brand, whiteLabel]}
features={{ pdfExport: true, whiteLabel: true }}
saveDescription="Saved to your workspace."
onChange={setTemplate}
onSave={async (t) => {
const res = await fetch("/api/templates/investment-review", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(t)
});
if (!res.ok) throw new Error(`Save failed (${res.status})`);
}}
onValidate={setIssues}
/>
</div>
);
}The template prop is treated as the initial document plus a "load this" signal: passing a different object than the one the editor last emitted reloads it, so you can implement "open template" without remounting. Passing back the object you received from onChange does nothing, which is what makes the controlled-looking pattern above safe.
The ref exposes the full imperative API — getTemplate(), setTemplate(), setTheme(), setMode(), undo(), redo(), validate(), exportPdf(), print(), getLayout(), registerBlock() — from the first render after mount.
<ReportPreview />
Read-only and paginated, through the same pipeline as the PDF. onLayout fires after each pagination:
import { useState } from "react";
import { ReportPreview } from "@broadpaper/react";
import type { ReportTemplate } from "@broadpaper/core";
import { dataSources } from "./data-sources";
import { exampleData } from "./sample-data";
import { brand } from "./themes";
export function PreviewPane({ template }: { template: ReportTemplate }) {
const [pages, setPages] = useState(0);
return (
<>
<p>{pages} page(s)</p>
<ReportPreview
template={template}
data={exampleData}
theme={brand}
dataSources={dataSources}
zoom={0.8}
onLayout={(r) => setPages(r.paged.totalPages)}
/>
</>
);
}Pass mode="design" to show binding tokens instead of values.
Custom inspector UIs
Blocks describe their inspector declaratively (see Custom blocks). When you need a bespoke control, wrap a React component with reactInspector, which receives the field's value and a setter:
import { reactInspector } from "@broadpaper/react";
import type { InspectorComponent } from "@broadpaper/core";
export const colourScale: InspectorComponent = reactInspector(({ value, setValue }) => (
<input type="color" value={typeof value === "string" ? value : "#000000"} onChange={(e) => setValue(e.target.value)} />
));Then reference it from the block's inspector:
{ kind: "custom", prop: "scale", label: "Scale", component: colourScale }Sizing
The editor fills its container; give the wrapper a height. It is a CSS grid with three columns (blocks, canvas, inspector) and is tested down to 1120 px wide. Panels can be collapsed from the toolbar, which is what makes narrower embeddings workable.
Licensing
The designer, the preview and the viewer never check a licence: only the PDF does. If your React app exports PDFs in the browser, configure the key once in its bootstrap. The browser has no environment, so the value comes from your build:
import { BroadPaper } from "@broadpaper/forme";
BroadPaper.configure({ license: import.meta.env.VITE_BROADPAPER_LICENSE });Without one, PDFs carry the evaluation mark and the console says so once per page load. A free 60-day evaluation key from broadpaper.com/eval removes the mark while you evaluate. It needs BroadPaper 1.0.0 or later. Production use needs a paid licence, configured the same way. A key is a signed certificate rather than a credential, so it is safe in a browser bundle. See Licensing.