Skip to content

Dragble/dragble-react-editor

Repository files navigation

Dragble - AI-Powered React Email Template Builder

npm version license

dragble-react-editor

The fully AI-powered React editor for email templates and landing pages. Your end-users design visually with drag-and-drop — or describe what they want and watch AI agents build it live on the canvas. Powered by the built-in Model Context Protocol (MCP) server, connect Claude Code, OpenCode, Codex, Cursor, or your own AI backend directly to the editor. Structured tool calls mean guaranteed-valid output — no prompt engineering, no JSON hallucination, no broken layouts.

Dragble brings two design experiences together in one React component: a polished visual editor for designers and a conversational AI surface for everyone else — backed by structured tool calls that produce guaranteed-valid HTML emails and landing pages every time.

Website | Documentation | Dashboard

Dragble - AI-Powered React Email Editor with Drag and Drop

Features

  • Drag-and-drop email template builder with 20+ content blocks
  • Fully AI-powered via MCP — connect AI agents (Claude Code, OpenCode, Codex, Cursor) or your own AI backend to build designs live on the canvas. Structured tool calls mean guaranteed-valid output — no prompt engineering, no JSON hallucination
  • Responsive HTML email output compatible with all major email clients
  • Newsletter editor with merge tags, dynamic content, and display conditions
  • Visual email designer — no HTML/CSS knowledge required for end users
  • Export to HTML, JSON, image, PDF, or ZIP
  • Built-in image editor, AI content generation, and collaboration tools
  • Full TypeScript support
  • Lightweight React wrapper — just a single component or hook

Installation

# npm
npm install dragble-react-editor

# yarn
yarn add dragble-react-editor

# pnpm
pnpm add dragble-react-editor

Editor Key

An editorKey is required to use the editor. You can get one by creating a project on the Dragble Developer Dashboard.

Quick Start

import { useRef } from "react";
import {
  DragbleEditor,
  DragbleEditorRef,
  DesignJson,
} from "dragble-react-editor";

function EmailBuilder() {
  const editorRef = useRef<DragbleEditorRef>(null);

  const handleChange = async (data: { design: DesignJson; type: string }) => {
    // Design JSON is available directly from the callback
    const json = data.design;
    console.log("Design JSON:", json);

    // To get HTML, call exportHtml on the editor
    const html = await editorRef.current?.editor?.exportHtml();
    console.log("HTML:", html);
  };

  return (
    <div style={{ height: "100vh" }}>
      <DragbleEditor
        ref={editorRef}
        editorKey="your-editor-key"
        editorMode="email"
        minHeight="600px"
        onReady={(editor) => console.log("Editor ready!")}
        onChange={handleChange}
        onError={(error) => console.error("Editor error:", error)}
      />
    </div>
  );
}

Complete Example

import { useRef, useState, useCallback } from "react";
import {
  DragbleEditor,
  DragbleEditorRef,
  DesignJson,
} from "dragble-react-editor";

function AdvancedEmailBuilder() {
  const editorRef = useRef<DragbleEditorRef>(null);
  const [isDirty, setIsDirty] = useState(false);

  const handleReady = useCallback((editor) => {
    // Set merge tags (must pass a MergeTagsConfig object)
    editor.setMergeTags({
      customMergeTags: [
        { name: "First Name", value: "{{first_name}}" },
        { name: "Last Name", value: "{{last_name}}" },
        { name: "Company", value: "{{company}}" },
      ],
      excludeDefaults: false,
      sort: true,
    });

    // Set custom fonts
    editor.setFonts({
      showDefaultFonts: true,
      customFonts: [{ label: "Brand Font", value: "BrandFont, sans-serif" }],
    });

    // Load saved design if available
    const savedDesign = localStorage.getItem("email-design");
    if (savedDesign) {
      editor.loadDesign(JSON.parse(savedDesign));
    }
  }, []);

  const handleChange = useCallback(
    (data: { design: DesignJson; type: string }) => {
      setIsDirty(true);
      localStorage.setItem("email-design", JSON.stringify(data.design));
    },
    [],
  );

  const handleExportHtml = async () => {
    const editor = editorRef.current?.editor;
    if (!editor) return;

    const html = await editor.exportHtml();
    const blob = new Blob([html], { type: "text/html" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "email.html";
    a.click();
  };

  const handleExportImage = async () => {
    const editor = editorRef.current?.editor;
    if (!editor) return;

    const data = await editor.exportImage();
    window.open(data.url, "_blank");
  };

  return (
    <div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
      <div
        style={{
          padding: 12,
          borderBottom: "1px solid #ddd",
          display: "flex",
          gap: 8,
        }}
      >
        <button onClick={() => editorRef.current?.editor?.undo()}>Undo</button>
        <button onClick={() => editorRef.current?.editor?.redo()}>Redo</button>
        <button
          onClick={() => editorRef.current?.editor?.showPreview("desktop")}
        >
          Preview
        </button>
        <button onClick={handleExportHtml}>Export HTML</button>
        <button onClick={handleExportImage}>Export Image</button>
        {isDirty && <span style={{ color: "orange" }}>Unsaved changes</span>}
      </div>

      <DragbleEditor
        ref={editorRef}
        editorKey="your-editor-key"
        editorMode="email"
        height="100%"
        designMode="live"
        options={{
          appearance: { theme: "light" },
          features: {
            preview: true,
            undoRedo: true,
            imageEditor: true,
          },
        }}
        onReady={handleReady}
        onChange={handleChange}
        onError={(error) => console.error(error.message)}
      />
    </div>
  );
}

export default AdvancedEmailBuilder;

MCP — AI Integration

Connect AI agents (Claude Code, OpenCode, Codex, Cursor, or your own backend) to the editor through the Model Context Protocol. The AI calls structured tools — add_row, add_heading, update_button, export_html — that mutate design state live on the canvas.

Enabling MCP

MCP is off by default. Set features: { mcp: true } to opt in:

<DragbleEditor
  ref={editorRef}
  editorKey="db_pxl81cxn92wignwx"
  options={{ features: { mcp: true } }}
/>

MCP also requires a Starter plan or higher. Both conditions must be true — plan allows it AND SDK enables it.

Quick example — your backend controls the AI

import { useRef } from "react";
import { DragbleEditor, DragbleEditorRef } from "dragble-react-editor";

function App() {
  const editorRef = useRef<DragbleEditorRef>(null);

  const handleConnectAI = async () => {
    // The id is YOUR identifier — derive it from your own database/session
    // so the same user editing the same document always gets the same MCP
    // session. Example: if your logged-in user is "alice123" and they're
    // editing document "campaign-summer-2026", build an id like this:
    //
    //   const id = "alice123-campaign-summer-2026";
    //
    // Format rules: 8-128 chars, only letters/digits/hyphens/underscores.
    const userIdFromAuth = "alice123"; // from your auth/session
    const docIdFromRoute = "campaign-summer"; // from your URL or DB row
    const id = `${userIdFromAuth}-${docIdFromRoute}`;
    const { sessionId } = await editorRef.current!.editor!.connectMCP({ id });
    // Pass sessionId to your backend — it calls MCP tools with your mcp_key
  };

  return (
    <div style={{ height: "100vh" }}>
      <button onClick={handleConnectAI}>Connect AI</button>
      <DragbleEditor
        ref={editorRef}
        editorKey="db_pxl81cxn92wignwx"
        options={{ features: { mcp: true } }}
      />
    </div>
  );
}

Quick example — end-user pairs their own AI client

const handleLetUserPair = async () => {
  const editor = editorRef.current!.editor!;
  // Same id you'd use anywhere else for this user+document combination.
  // 8-128 chars, only letters/digits/hyphens/underscores.
  const id = "alice123-campaign-summer-2026";
  await editor.connectMCP({ id });

  // Explicitly generate a pairing code (not auto-generated)
  const { code, expiresAt } = await editor.getPairingCode();
  alert(`Paste this into Claude Code: ${code}`);
};

One controller per session

Each session can be controlled by either your backend or an end-user's AI client (OpenCode, Claude Code), never both at the same time:

  • If your backend makes the first tool call → session is locked to backend. Pairing codes are rejected.
  • If a user pairs via pairing code first → session is locked to paired client. Backend tool calls are rejected.

This prevents two AI controllers from conflicting on the same design.

How it works

  1. Enable MCP in the SDK config: features: { mcp: true }.
  2. Generate an MCP key in the Dragble dashboard: Project → MCP Key → Generate. Store it in your backend env vars — never in browser code.
  3. Call editor.connectMCP({ id }) where id is a stable identifier you control (see below).
  4. Choose your AI path: either your backend calls MCP tools directly (using the mcp_key), or you generate a pairing code for the end-user to connect their own AI client.
  5. Mutations stream live onto the editor canvas as the AI works.

The id parameter — why it matters

The id you pass to connectMCP() is a Bring Your Own ID (BYOI) that maps to your domain entities. It is NOT a random token — it is how Dragble identifies the session across browser refreshes, server restarts, and device switches.

Rules:

  • 8–128 characters long
  • Only letters, numbers, hyphens, and underscores (a-z A-Z 0-9 - _)
  • Must be deterministic — the same user editing the same document should always produce the same id

Why these rules?

  • The id is used in database lookups and URL paths — special characters or extreme lengths would break routing
  • Same id = resume the same session. Random UUIDs mean every page refresh creates a new session and loses AI context
  • Short IDs (< 8 chars) are too easy to guess, long IDs (> 128 chars) waste storage
// Recommended: derive from your domain — concrete examples
editor.connectMCP({ id: "alice123-campaign-summer-2026" }); // user + doc
editor.connectMCP({ id: "workspace_acme_template_welcome" }); // workspace + template
editor.connectMCP({ id: "org-uber-eats-promo-q4-2026" }); // org + campaign
editor.connectMCP({ id: "tenant_42_invoice_template_v3" }); // tenant + entity

// Valid but NOT recommended — random IDs break session continuity
// (every page refresh creates a brand new session, AI loses context)
editor.connectMCP({ id: crypto.randomUUID() });

Disconnecting

disconnectMCP() permanently destroys the session — the session cannot be reopened:

const { destroyed } = await editor.disconnectMCP();

Your backend can also force-destroy a session server-side (e.g., when a user's subscription ends):

curl -X DELETE https://mcp.dragble.com/sessions/user-42-doc-99 \
  -H "X-API-Key: db_mcp_your_key_here"

Idle sessions are reaped after 2 hours of inactivity. Active sessions never expire — each tool call resets the timer.

MCP method reference

Method Returns
editor.connectMCP({ id, editorMode? }) { sessionId, resumed? }
editor.disconnectMCP() { destroyed } — permanently deletes session
editor.getPairingCode() { code, expiresAt } — generate a pairing code for end-user AI clients
editor.endPairing() { revoked } — invalidate the active pairing code
editor.getMCPStatus() { paired: true, sessionId } | { paired: false, reason? }
editor.onAIToolFired(cb) unsubscribe fn — fires when AI calls any tool

Full documentation

Props

Prop Type Required Default Description
editorKey string Yes Editor key for authentication
design DesignJson | ModuleData | null No undefined Initial design to load
editorMode EditorMode No "email" | "web" | "popup"
contentType "module" No Single-row module editor mode
options EditorOptions No {} All editor configuration
popup PopupConfig No Popup config (only when editorMode is "popup")
collaboration boolean | CollaborationFeaturesConfig No Collaboration features
user UserInfo No User info for session/collaboration
designMode "edit" | "live" No "live" Template permissions mode
height string | number No Editor height
minHeight string | number No "600px" Minimum editor height
callbacks Omit<DragbleCallbacks, "onReady" | "onLoad" | "onChange" | "onError"> No SDK callbacks (excluding those handled by dedicated props)
className string No CSS class for the outer container
style React.CSSProperties No Inline styles for the outer container

| onReady | (editor: DragbleSDK) => void | No | — | Called when the editor is ready | | onLoad | () => void | No | — | Called when a design is loaded | | onChange | (data: { design: DesignJson; type: string }) => void | No | — | Called when the design changes | | onError | (error: Error) => void | No | — | Called on error | | onComment | (action: CommentAction) => void | No | — | Called on comment events |

Ref

Use a ref to access the SDK instance:

const editorRef = useRef<DragbleEditorRef>(null);

// DragbleEditorRef shape:
// {
//   editor: DragbleSDK | null;
//   isReady: () => boolean;
// }

Hook API

The useDragbleEditor hook provides a convenient way to access the editor:

import { DragbleEditor, useDragbleEditor } from "dragble-react-editor";

function MyEditor() {
  const { ref, editor, isReady } = useDragbleEditor();

  return (
    <div>
      <button
        onClick={async () => {
          const html = await editor?.exportHtml();
          console.log(html);
        }}
        disabled={!isReady}
      >
        Export
      </button>
      <DragbleEditor ref={ref} editorKey="your-editor-key" />
    </div>
  );
}

Returns: { ref, editor, isReady }ref is passed to the component, editor is the SDK instance (or null), and isReady is a boolean.

SDK Methods Reference

Access the SDK via editorRef.current?.editor or the editor value from useDragbleEditor(). All export and getter methods return Promises.

Design

editor.loadDesign(design, options?);                   // void
const result = await editor.loadDesignAsync(design, options?);
// => { success, validRowsCount, invalidRowsCount, errors? }
editor.loadBlank(options?);                            // void
const { html, json } = await editor.getDesign();       // Promise

Export

All export methods are Promise-based. There are no callback overloads.

const html = await editor.exportHtml(options?);        // Promise<string>
const json = await editor.exportJson();                // Promise<DesignJson>
const text = await editor.exportPlainText();           // Promise<string>
const imageData = await editor.exportImage(options?);  // Promise<ExportImageData>
const pdfData = await editor.exportPdf(options?);      // Promise<ExportPdfData>
const zipData = await editor.exportZip(options?);      // Promise<ExportZipData>
const values = await editor.getPopupValues();          // Promise<PopupValues | null>

Merge Tags

setMergeTags accepts a MergeTagsConfig object, not a plain array.

editor.setMergeTags({
  customMergeTags: [
    { name: "First Name", value: "{{first_name}}" },
    { name: "Company", value: "{{company}}" },
  ],
  excludeDefaults: false,
  sort: true,
});
const tags = await editor.getMergeTags(); // Promise<(MergeTag | MergeTagGroup)[]>

Special Links

setSpecialLinks accepts a SpecialLinksConfig object.

editor.setSpecialLinks({
  customSpecialLinks: [{ name: "Unsubscribe", href: "{{unsubscribe_url}}" }],
  excludeDefaults: false,
});
const links = await editor.getSpecialLinks(); // Promise<(SpecialLink | SpecialLinkGroup)[]>

Modules

editor.setModules(modules); // void
editor.setModulesLoading(loading); // void
const modules = await editor.getModules(); // Promise<Module[]>

Fonts

editor.setFonts(config); // void
const fonts = await editor.getFonts(); // Promise<FontsConfig>

Body Values

editor.setBodyValues({
  backgroundColor: "#f5f5f5",
  contentWidth: "600px",
});
const values = await editor.getBodyValues(); // Promise<SetBodyValuesOptions>

Editor Configuration

editor.setOptions(options); // void — Partial<EditorOptions>
editor.setToolsConfig(toolsConfig); // void
editor.setEditorMode(mode); // void
editor.setEditorConfig(config); // void
const config = await editor.getEditorConfig(); // Promise<EditorBehaviorConfig>

Locale, Language & Text Direction

editor.setLocale(locale, translations?);            // void
editor.setLanguage(language);                       // void
const lang = await editor.getLanguage();            // Promise<Language | null>
editor.setTextDirection(direction);                 // void — 'ltr' | 'rtl'
const dir = await editor.getTextDirection();        // Promise<TextDirection>

Appearance

editor.setAppearance(appearance); // void

Undo / Redo / Save

editor.undo(); // void
editor.redo(); // void
const canUndo = await editor.canUndo(); // Promise<boolean>
const canRedo = await editor.canRedo(); // Promise<boolean>
editor.save(); // void

Preview

editor.showPreview(device?);  // void — 'desktop' | 'tablet' | 'mobile'
editor.hidePreview();         // void

Custom Tools

await editor.registerTool(config); // Promise<void>
await editor.unregisterTool(toolId); // Promise<void>
const tools = await editor.getTools(); // Promise<Array<{ id, label, baseToolType }>>

Custom Widgets

await editor.createWidget(config); // Promise<void>
await editor.removeWidget(widgetName); // Promise<void>

Collaboration & Comments

editor.showComment(commentId); // void
editor.openCommentPanel(rowId); // void

Tabs & Branding

editor.updateTabs(tabs); // void
editor.setBrandingColors(config); // void
editor.registerColumns(cells); // void

Display Conditions

editor.setDisplayConditions(config); // void

Audit

const result = await editor.audit(options?);  // Promise<AuditResult>

Asset Management

const { success, url, error } = await editor.uploadImage(file, options?);
const { assets, total } = await editor.listAssets(options?);
const { success, error } = await editor.deleteAsset(assetId);
const folders = await editor.listAssetFolders(parentId?);
const folder = await editor.createAssetFolder(name, parentId?);
const info = await editor.getStorageInfo();

Status & Lifecycle

editor.isReady(); // boolean
editor.destroy(); // void

Events

Subscribe to editor events using addEventListener:

const unsubscribe = editor.addEventListener("design:updated", (data) => {
  console.log("Design changed:", data);
});

// Or remove manually
editor.removeEventListener("design:updated", callback);

Available Events

Event Description
editor:ready Editor initialized
design:loaded Design loaded
design:updated Design changed
design:saved Design saved
row:selected Row selected
row:unselected Row unselected
column:selected Column selected
column:unselected Column unselected
content:selected Content block selected
content:unselected Content block unselected
content:modified Content block modified
content:added Content block added
content:deleted Content block deleted
preview:shown Preview opened
preview:hidden Preview closed
image:uploaded Image uploaded successfully
image:error Image upload error
export:html HTML exported
export:plainText Plain text exported
export:image Image exported
save Save triggered
save:success Save succeeded
save:error Save failed
template:requested Template requested
element:selected Element selected
element:deselected Element deselected
export Export triggered
displayCondition:applied Display condition applied
displayCondition:removed Display condition removed
displayCondition:updated Display condition updated

TypeScript

All SDK types are re-exported from the package:

import type {
  DragbleEditorRef,
  DragbleEditorProps,
  DesignJson,
  EditorOptions,
  MergeTag,
  MergeTagGroup,
  MergeTagsConfig,
  SpecialLink,
  SpecialLinkGroup,
  SpecialLinksConfig,
  FontsConfig,
  EditorMode,
  PopupConfig,
  UserInfo,
  CollaborationFeaturesConfig,
  CommentAction,
} from "dragble-react-editor";

Contributing

See CONTRIBUTING.md for guidelines on how to contribute to this project.

License

MIT

About

AI Powered Embeddable email editor for React — build responsive HTML email templates, newsletters, and marketing campaigns with drag-and-drop

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors