BuilderJS

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

Explore