Installation
Install the package, then mount it — in React, Symfony, Laravel, or a plain HTML page.
Which frameworks this works with
Your backend language does not matter. Python, PHP, Java, Go, Ruby, .NET — they all send HTML, and the builder runs in the browser that receives it. There is no server component, so there is nothing to be compatible with.
What actually decides the integration is whether your front end has a bundler.
| Your stack | Use | Notes |
|---|---|---|
| React — Next.js, Vite, CRA, Remix | npm package | The component directly. Client-side only — see the SSR note below. |
| Symfony | npm + UX React | Props come from PHP; no mount code. Or the script tag, if you would rather not add React. |
| Laravel, Rails, Django, Flask, FastAPI, Spring, .NET, Go, plain PHP | pdf-builder.bundle.min.js |
Two tags and a div. No npm, no build step, no React in your project. |
| Vue, Svelte, Angular | Script tag, or the module build | Mount into an element from your component's lifecycle hook, and destroy() on unmount. |
| WordPress, or any CMS | Script tag | Enqueue the two files; nothing else changes. |
| iOS / Android | Script tag inside a WebView | Works, with real limits — read the next section before committing. |
dynamic(…, { ssr: false }); elsewhere mount it after the
page has loaded.
Vue, Svelte and Angular
Phones, tablets and native shells
Stated plainly, because this is worth knowing before you promise it to anyone.
| Device | Verdict | What actually happens |
|---|---|---|
| Desktop / laptop | Full | Three panels, drag-and-drop, everything. |
| Tablet | Full | Below 880px the side panels become slide-over drawers and the canvas takes the width. Touch dragging works — the handles set touch-action: none so a drag is not stolen by the scroller. |
| Phone | Usable, not comfortable | The same drawers, but an A4 page is 794px wide and a phone is around 390px, so the canvas scrolls sideways. Editing works; laying out a document does not feel good. |
| Native shell (Hotwire Native, WKWebView, Android WebView) | Same as the browser | It is the same web view. Nothing extra is needed, and nothing extra is gained. |
Install
React is a peer dependency, which means the package deliberately does not bring its own copy. If two copies of React end up in one page, hooks break at runtime with an error that looks nothing like its cause — so the package uses whichever React your application already has.
A React app
The whole integration. There is no provider to wrap and no store to configure.
styles.css the builder renders as unstyled markup — usually a tall column
of plain text that looks broken rather than unstyled. If your builder looks wrong before you
have changed anything, check this first.
Symfony
The cleanest route is Symfony UX React, because props come straight from PHP and there is no mount code to maintain.
1. Install the bridge
2. Register the React components
assets/bootstrap.js by that exact name.
If your project calls the file something else — stimulus_bootstrap.js, say — the
line above is silently never added. The symptom is a <div> that renders
with its data-controller attributes intact and nothing inside it, and no error
anywhere. Check this file before you check anything else.
3. Write the component
4. Render it from Twig
encore_entry_link_tags('css/app'), the builder's CSS is compiled on every build
and then never linked. Add encore_entry_link_tags('js/app') as well.
The locale. app.request.locale returns en_IN, with
an underscore. That is a POSIX locale, not a BCP 47 language tag, and Intl
rejects it outright. The package converts it for you, but anything else in your stack that
formats dates or numbers will not — pass en-IN where you can.
Plain HTML or PHP — no npm, no build step
If your page is hand-written HTML, or PHP that prints HTML, you need no npm, no bundler and no React. Add a stylesheet and a script, exactly as you would add Bootstrap.
From the CDN
Nothing to download. Two tags, the same way you add Bootstrap — jsDelivr serves every npm package automatically, so these URLs work the moment a version is published.
@1.0.0 is a file that can never change. Drop it, or write @latest,
and jsDelivr serves whatever is newest — so a release you have never tested can reach your
production page without anybody deploying anything.
cdn.jsdelivr.net/npm/PACKAGE@VERSION/PATH for every
public package. There is no CDN to configure and no account to create — publishing to npm is
what creates the URL. The same is true of Bootstrap's.
Self-hosted, if you prefer
That is it. A global called PdfBuilder appears, with
create() on it. No type="module", no import, nothing to
install.
With integrity hashes
If you serve the files from a CDN, pin them the way Bootstrap does, so a tampered file is refused rather than executed:
openssl dgst -sha384 -binary FILE | openssl base64 -A, or drop the
integrity attribute while you are still iterating.
Two builds, and which to use
| File | First load | Use when |
|---|---|---|
pdf-builder.bundle.min.jsclassic script |
645 KB gzipped 2.0 MB raw |
Simplest. One tag, a global, works everywhere. Everything is inside it, including the PDF renderer. |
pdf-builder.standalone.jsmodule |
~250 KB gzipped 856 KB raw |
Lighter. Needs type="module" and an import. The 2.3 MB PDF renderer downloads only when someone actually exports. |
.bundle. means "the dependencies are inside this file". Start with the bundle;
move to the module build if the first load matters more than the extra line of code.
Loading a saved document from PHP
What you get back
create() takes a CSS selector or an element, and the same options as every other
route — onSave, onUploadImage, resolveImageUrl,
variables, theme and the rest. Everything in this manual applies.
The module build, if you prefer it
file://.
Opening the HTML by double-clicking gives a CORS error. Serve it over http:// —
your existing web server is fine. The classic build above has no such restriction.
Which files to copy onto your server
| If you use… | Copy these from the package's dist/ folder |
|---|---|
| The classic script | pdf-builder.bundle.min.js and pdf-builder.css. Two files, nothing else. |
| The module build | pdf-builder.standalone.js, pdf-builder.css, and the chunks beside them —
react-pdf.browser-*.js, CapturedDocument-*.js, capture-*.js,
jsx-runtime-*.js. You never reference the chunks yourself; they load on demand.
Delete them and PDF export silently stops working. |
Put them in one directory — /assets/pdf-builder/, say — and point the two tags at it.
Laravel, Rails, or a bundler
Your app does not have to be a React app. Render an empty element from your template and mount into it from one small entry file. Nothing else on your site becomes React, and the React runtime only loads on that page.
Giving it a height
The builder fills its container. If that container has no height you get a builder with no height, which looks exactly like a failed load. This catches almost everyone once.
height: 80vh, height: calc(100vh - 64px), or a flex parent that
gives it a real size.
height: auto and wonder why the canvas is a sliver.
Install-time gotchas
| Symptom | Cause and fix |
|---|---|
| "Invalid hook call" at runtime | Two copies of React. Check npm ls react. With Vite, add every React entrypoint to optimizeDeps.include so they share one pre-bundle generation — three different ?v= hashes in a stack trace means three generations. |
| Unstyled markup, no layout | styles.css was never imported, or its build output was never linked. See the Symfony note above. |
Empty <div>, no console error |
With Symfony UX React: the component was never registered. With a manual mount: the element did not exist when the script ran — load it as a module, or mount after DOMContentLoaded. |
| Builder is one pixel tall | The container has no height. |
| Server-side rendering crashes | The builder is a browser component — it measures real elements to lay pages out. Render it client-side only: dynamic(…, { ssr: false }) in Next.js. |
| Package changes do not appear after a rebuild | If you install it from a local tarball, Yarn 1 caches it by name+version and keeps serving the first one it saw. Bump the version, or extract the tarball into node_modules yourself. |