React integration
@broadpaper/react wraps the framework-neutral editor and preview. Props mirror ReportDesignerOptions; the ref exposes the imperative API.
<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.