Embed a Drag & Drop Builder in Next.js (App Router)
BuilderJS is a vanilla-JavaScript, framework-agnostic drag-and-drop editor for HTML email and pages that you self-host and embed in your own app. This guide shows the correct, copy-paste pattern for Next.js using the App Router. The key thing to internalize up front: BuilderJS is not an npm package and has no module export. You load a global browser script (builder.js plus builder.css) that attaches window.Builder, then instantiate it client-side inside a 'use client' component. There is no SSR path — it is a DOM-mount bundle, so the editor only mounts in the browser after hydration. You render three container nodes (widgets, canvas, settings), pass CSS selector strings to the constructor, and call load() with four positional arguments to hydrate a template. Persistence is plain JSON: getData() serializes the design, you POST it to your own backend, and load() restores it. BuilderJS is the editor you embed — it does not send, host, or publish; you bring your own sending and hosting.
Mount it
'use client';
import { useEffect, useRef } from 'react';
export default function BuilderEditor() {
const mounted = useRef(false);
useEffect(() => {
if (mounted.current || !window.Builder) return; // Strict Mode guard
mounted.current = true;
const builder = new window.Builder({
mainContainer: '#bjs-canvas', // required CSS selector
widgetsContainer: '#bjs-widgets', // optional
settingsContainer: '#bjs-settings' // optional
});
builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl);
// Save: const json = builder.getData(); POST json to your backend.
}, []);
return (
<>
<div id="bjs-widgets" />
<div id="bjs-canvas" />
<div id="bjs-settings" />
</>
);
}
It's a global window.Builder script, not an npm import
BuilderJS ships as a self-hosted browser bundle, not a module. You add it once — either in your root layout or via next/script — with a stylesheet link and a script tag: <link rel="stylesheet" href="/builderjs/builder.css"> and <script src="/builderjs/builder.js"></script>. Loading that script attaches the global window.Builder constructor. There is no named export, so do NOT write import { Builder } from anything — that will fail because no such module exists. In Next.js the cleanest approach is next/script with strategy="afterInteractive" and an onLoad handler that runs your mount, or place the tags in app/layout.tsx so the global is present before your client component runs. Either way you reference window.Builder at runtime, never an import.
Mount is client-side only — no SSR
BuilderJS is a DOM-mount bundle: it queries the DOM and builds the editor chrome the moment it is constructed, so it can only run in the browser. In the App Router that means the component doing the mount must start with 'use client', and the actual new Builder({...}) call must happen after the page has hydrated — inside the next/script onLoad callback or a useEffect. Never call it during render or on the server. Render your three container divs first (<div id="bjs-widgets">, <div id="bjs-canvas">, <div id="bjs-settings">), and pass them to the constructor as CSS selector strings: { mainContainer: '#bjs-canvas', widgetsContainer: '#bjs-widgets', settingsContainer: '#bjs-settings' }. mainContainer is required; the other two are optional. Note these are selector strings — not DOM nodes, not refs — and there is no 'container' or 'el' option.
Persisting designs with getData() and load()
Persistence is intentionally plain. const json = builder.getData() returns a serializable snapshot of the current design. You POST that JSON to your own backend route (e.g. a Next.js Route Handler under app/api) and store it however you like. To restore, call builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) — exactly four positional arguments, in that order: the saved theme JSON, the theme templates JSON, the theme config data, and the media base URL for asset resolution. The same load() call seeds a fresh editor with a starter template or rehydrates a previously saved one. Because the format is just JSON, you control the storage layer entirely — database, object storage, or a file; BuilderJS does not impose a persistence backend.
React 18 Strict Mode and lifecycle
There is no destroy(), teardown(), or dispose() method on BuilderJS — and the prose should not pretend otherwise. On a real unmount, React discards the container DOM node and the editor goes with it; there is nothing to clean up manually. The one thing to handle is React 18 Strict Mode in development, which intentionally double-invokes effects. Guard your instantiation with a ref flag (a simple mountedRef.current check) so you construct exactly one Builder instance even when the effect or onLoad fires twice. Do not call builder.destroy() to 'reset' between invocations — the method does not exist; just instantiate once and let React's normal unmount discard the DOM.
Frequently asked questions
-
How do I install BuilderJS in a Next.js project — npm install?
No. BuilderJS is not an npm package. You self-host the compiled builder.js and builder.css (e.g. under public/builderjs/) and load them as a global browser script and stylesheet. Loading builder.js attaches window.Builder. There is no module export, so never write import { Builder } from anything.
-
Can I render the editor on the server (SSR)?
No. BuilderJS is a client-side DOM-mount bundle. It must run in the browser after hydration. Use a 'use client' component and instantiate inside next/script's onLoad or a useEffect — never during render or on the server.
-
What goes in the Builder constructor options?
CSS selector strings: { mainContainer: '#bjs-canvas' } is required; widgetsContainer and settingsContainer are optional. They are selector strings that BuilderJS resolves with querySelector — not DOM nodes and not React refs. There is no 'container' or 'el' option.
-
How do I save and reload a design?
Call builder.getData() to get a JSON snapshot, POST it to your own backend, and store it. To restore, call builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) with those four positional arguments in that exact order. BuilderJS does not host or store designs for you.
-
How do I clean up the editor on unmount, and why does it mount twice in dev?
There is no destroy() or dispose() method — on unmount React discards the container DOM node and the editor with it, so no manual teardown is needed. The double-mount in development is React 18 Strict Mode's intentional effect double-invoke; guard instantiation with a ref flag so you create exactly one Builder instance.