React component for building email templates with drag-and-drop. Embed a full-featured email editor into your React app — create responsive HTML emails, newsletters, transactional email templates, and email marketing campaigns visually without writing code.
Pexelize is a modern email builder and email template editor that lets your users design professional emails with a visual drag-and-drop interface.
Website | Documentation | Dashboard
- Drag-and-drop email template builder with 20+ content blocks
- 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
# npm
npm install @pexelize/react-email-editor
# yarn
yarn add @pexelize/react-email-editor
# pnpm
pnpm add @pexelize/react-email-editorAn editorKey is required to use the editor. You can get one by creating a project on the Pexelize Developer Dashboard.
import { useRef } from "react";
import { PexelizeEditor, PexelizeEditorRef } from "@pexelize/react-email-editor";
function EmailBuilder() {
const editorRef = useRef<PexelizeEditorRef>(null);
const handleExport = async () => {
const editor = editorRef.current?.editor;
if (!editor) return;
const html = await editor.exportHtml();
console.log("HTML:", html);
};
return (
<div style={{ height: "100vh" }}>
<button onClick={handleExport}>Export HTML</button>
<PexelizeEditor
ref={editorRef}
editorKey="your-editor-key"
editorMode="email"
minHeight="600px"
onReady={(editor) => console.log("Editor ready!")}
onChange={(data) => console.log("Design changed:", data.type)}
onError={(error) => console.error("Editor error:", error)}
/>
</div>
);
}import { useRef, useState, useCallback } from "react";
import {
PexelizeEditor,
PexelizeEditorRef,
DesignJson,
} from "@pexelize/react-email-editor";
function AdvancedEmailBuilder() {
const editorRef = useRef<PexelizeEditorRef>(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>
<PexelizeEditor
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;| 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<PexelizeCallbacks, "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: PexelizeSDK) => 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 |
Use a ref to access the SDK instance:
const editorRef = useRef<PexelizeEditorRef>(null);
// PexelizeEditorRef shape:
// {
// editor: PexelizeSDK | null;
// isReady: () => boolean;
// }The usePexelizeEditor hook provides a convenient way to access the editor:
import { PexelizeEditor, usePexelizeEditor } from "@pexelize/react-email-editor";
function MyEditor() {
const { ref, editor, isReady } = usePexelizeEditor();
return (
<div>
<button
onClick={async () => {
const html = await editor?.exportHtml();
console.log(html);
}}
disabled={!isReady}
>
Export
</button>
<PexelizeEditor 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.
Access the SDK via editorRef.current?.editor or the editor value from usePexelizeEditor(). All export and getter methods return Promises.
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(); // PromiseAll 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>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)[]>setSpecialLinks accepts a SpecialLinksConfig object.
editor.setSpecialLinks({
customSpecialLinks: [{ name: "Unsubscribe", href: "{{unsubscribe_url}}" }],
excludeDefaults: false,
});
const links = await editor.getSpecialLinks(); // Promise<(SpecialLink | SpecialLinkGroup)[]>editor.setModules(modules); // void
editor.setModulesLoading(loading); // void
const modules = await editor.getModules(); // Promise<Module[]>editor.setFonts(config); // void
const fonts = await editor.getFonts(); // Promise<FontsConfig>editor.setBodyValues({
backgroundColor: "#f5f5f5",
contentWidth: "600px",
});
const values = await editor.getBodyValues(); // Promise<SetBodyValuesOptions>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>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>editor.setAppearance(appearance); // voideditor.undo(); // void
editor.redo(); // void
const canUndo = await editor.canUndo(); // Promise<boolean>
const canRedo = await editor.canRedo(); // Promise<boolean>
editor.save(); // voideditor.showPreview(device?); // void — 'desktop' | 'tablet' | 'mobile'
editor.hidePreview(); // voidawait editor.registerTool(config); // Promise<void>
await editor.unregisterTool(toolId); // Promise<void>
const tools = await editor.getTools(); // Promise<Array<{ id, label, baseToolType }>>await editor.createWidget(config); // Promise<void>
await editor.removeWidget(widgetName); // Promise<void>editor.showComment(commentId); // void
editor.openCommentPanel(rowId); // voideditor.updateTabs(tabs); // void
editor.setBrandingColors(config); // void
editor.registerColumns(cells); // voideditor.setDisplayConditions(config); // voidconst result = await editor.audit(options?); // Promise<AuditResult>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();editor.isReady(); // boolean
editor.destroy(); // voidSubscribe 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);| 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 |
All SDK types are re-exported from the package:
import type {
PexelizeEditorRef,
PexelizeEditorProps,
DesignJson,
EditorOptions,
MergeTag,
MergeTagGroup,
MergeTagsConfig,
SpecialLink,
SpecialLinkGroup,
SpecialLinksConfig,
FontsConfig,
EditorMode,
PopupConfig,
UserInfo,
CollaborationFeaturesConfig,
CommentAction,
} from "@pexelize/react-email-editor";See CONTRIBUTING.md for guidelines on how to contribute to this project.
