npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@parathantl/react-email-editor

v0.1.19

Published

A visual MJML email template editor for React

Readme

@parathantl/react-email-editor

npm version license

A visual drag-and-drop email template editor for React, powered by MJML. Build responsive HTML emails with a rich block-based editor, real-time preview, and full MJML round-trip support.

Features

  • 12 block types: Text, Heading, Button, Image, Video, Divider, Spacer, Social, HTML, Countdown, Menu, Hero
  • Drag-and-drop block reordering and section management
  • Rich text editing (TipTap) with formatting toolbar
  • MJML generation, parsing, and HTML compilation
  • Template variable support ({{ variable }} syntax)
  • Built-in persistence with localStorage or custom adapters
  • Responsive editor UI with collapsible panels
  • Undo/redo history (50 steps)
  • Export to MJML, HTML, and PDF
  • Extensible via registry pattern (add custom block types)
  • Full TypeScript support with strict mode
  • CSS variables for easy theming

Installation

npm install @parathantl/react-email-editor

Peer dependencies: React 18+, React DOM 18+

Optional: Install mjml-browser for HTML compilation (MJML to HTML conversion):

npm install mjml-browser

Quick Start

import { EmailEditor } from '@parathantl/react-email-editor';
import '@parathantl/react-email-editor/styles.css';

function App() {
  return (
    <div style={{ height: '100vh' }}>
      <EmailEditor
        onChange={(template) => console.log(template)}
        onSave={(mjml, html) => console.log(mjml, html)}
      />
    </div>
  );
}

Persistence

Templates auto-save and restore when you provide a persistenceKey. Each key stores a separate template, so multiple editor instances can coexist.

localStorage (default)

<EmailEditor persistenceKey="campaign-123" />

Custom Adapter (server, IndexedDB, etc.)

import type { PersistenceAdapter } from '@parathantl/react-email-editor';

const serverAdapter: PersistenceAdapter = {
  save(key, template) {
    fetch(`/api/templates/${key}`, {
      method: 'PUT',
      body: JSON.stringify(template),
      headers: { 'Content-Type': 'application/json' },
    });
  },
  load(key) {
    // Must be synchronous — preload data before rendering the editor
    return window.__PRELOADED_TEMPLATES__?.[key] ?? null;
  },
  remove(key) {
    fetch(`/api/templates/${key}`, { method: 'DELETE' });
  },
};

<EmailEditor persistenceKey="campaign-123" persistenceAdapter={serverAdapter} />

Priority order: persisted data > initialTemplate > initialMJML

Loading MJML from Another Component

Use the ref API to control the editor from parent components:

import { useRef } from 'react';
import { EmailEditor } from '@parathantl/react-email-editor';
import type { EmailEditorRef } from '@parathantl/react-email-editor';

function TemplateDesigner() {
  const editorRef = useRef<EmailEditorRef>(null);

  const loadFromServer = async () => {
    const res = await fetch('/api/templates/welcome');
    const mjml = await res.text();
    editorRef.current?.loadMJML(mjml);
  };

  const handleSave = async () => {
    const mjml = editorRef.current?.getMJML();
    const html = await editorRef.current?.getHTML();
    // Send to your API
  };

  return (
    <div>
      <button onClick={loadFromServer}>Load Template</button>
      <button onClick={handleSave}>Save</button>
      <EmailEditor ref={editorRef} persistenceKey="welcome" />
    </div>
  );
}

Or pass MJML at init time without a ref:

<EmailEditor initialMJML={mjmlString} />

Ref API

| Method | Returns | Description | |--------|---------|-------------| | getMJML() | string | Get current template as MJML | | getHTML() | Promise<string> | Compile and get HTML output | | getJSON() | EmailTemplate | Get template as JSON object | | loadMJML(source) | void | Parse MJML and load into editor | | loadJSON(template) | void | Load an EmailTemplate object | | insertBlock(type, sectionIdx?) | void | Programmatically add a block | | getVariables() | string[] | Extract {{ variable }} keys | | undo() | void | Undo last action | | redo() | void | Redo last undone action | | reset() | void | Clear all content | | clearPersisted() | void | Remove saved data for the current key | | exportPDF() | Promise<void> | Generate PDF via print dialog |

Props

| Prop | Type | Description | |------|------|-------------| | initialTemplate | EmailTemplate | Initial template object | | initialMJML | string | Initial MJML string (parsed on mount) | | variables | Variable[] | Template variables for {{ }} insertion | | imageUploadAdapter | ImageUploadAdapter | Custom image upload handler | | onChange | (template: EmailTemplate) => void | Called on every template change (debounced 150ms) | | onSave | (mjml: string, html: string) => void | Called on Ctrl+S | | onReady | () => void | Called once after editor mounts | | onVariablesChange | (customVariables: Variable[]) => void | Called when user adds/removes custom variables | | fontFamilies | string[] | Custom font options for the toolbar | | fontSizes | string[] | Custom font size options | | persistenceKey | string | Key for auto-save/restore (enables persistence) | | persistenceAdapter | PersistenceAdapter | Custom storage adapter (defaults to localStorage) | | className | string | CSS class for the outer wrapper | | style | CSSProperties | Inline styles for the outer wrapper |

Custom Icons (customIcons)

You can pass customIcons to override built-in UI icons.
Every key is optional. If you skip a key, the editor uses its default emoji/icon.

<EmailEditor
  customIcons={{
    desktop: '🖥️',
    mobile: '📱',
    undo: '↩️',
    redo: '↪️',
    addSection: '➕',
    sidebar: '📚',
    properties: '⚙️',
    visual: '🎨',
    source: '🧾',
    preview: '👁️',
    sectionDrag: '↕️',
    sectionDuplicate: '📄',
    sectionRemove: '🗑️',
    blockDuplicate: '📄',
    blockRemove: '🗑️',
    previewDesktop: '🖥️',
    previewMobile: '📱',
    paletteText: '📝',
    paletteHeading: '🔤',
    paletteButton: '🔘',
    paletteImage: '🖼️',
    paletteVideo: '▶️',
    paletteDivider: '➖',
    paletteSpacer: '↕️',
    paletteSocial: '🌐',
    paletteHtml: '💻',
    paletteCountdown: '⏰',
    paletteMenu: '📋',
    paletteHero: '⭐',
  }}
/>

Available customIcons keys

| Key | Used In | |-----|---------| | desktop | Canvas desktop view toggle | | mobile | Canvas mobile view toggle | | undo | Canvas undo button | | redo | Canvas redo button | | addSection | Canvas "Add Section" button | | sidebar | Toolbar sidebar toggle button | | properties | Toolbar properties toggle button | | visual | Toolbar Visual tab icon | | source | Toolbar Source tab icon | | preview | Toolbar Preview tab icon | | sectionDrag | Section drag handle | | sectionDuplicate | Section duplicate action | | sectionRemove | Section remove action | | blockDuplicate | Block duplicate action | | blockRemove | Block remove action | | previewDesktop | Preview panel desktop toggle | | previewMobile | Preview panel mobile toggle | | paletteText | Block palette icon for Text | | paletteHeading | Block palette icon for Heading | | paletteButton | Block palette icon for Button | | paletteImage | Block palette icon for Image | | paletteVideo | Block palette icon for Video | | paletteDivider | Block palette icon for Divider | | paletteSpacer | Block palette icon for Spacer | | paletteSocial | Block palette icon for Social | | paletteHtml | Block palette icon for HTML | | paletteCountdown | Block palette icon for Countdown | | paletteMenu | Block palette icon for Menu | | paletteHero | Block palette icon for Hero |

Block Types

| Type | Description | MJML Output | |------|-------------|-------------| | text | Rich text with formatting | <mj-text> | | heading | Heading (h1–h4) with level selector | <mj-text><h2>...</h2></mj-text> | | button | Call-to-action button | <mj-button> | | image | Image with optional link | <mj-image> | | video | Video thumbnail with play overlay | <mj-image> (linked) | | divider | Horizontal line | <mj-divider> | | spacer | Vertical spacing | <mj-spacer> | | social | Social media icon links | <mj-social> | | html | Raw HTML content | <mj-text> | | countdown | Live countdown timer with digit boxes | <mj-text> (HTML table) | | menu | Navigation menu links | <mj-navbar> | | hero | Heading + subtext + CTA button | <mj-text> (composite HTML) |

Template Variables

Define variables and insert them into text blocks as {{ variable_name }}. Variables appear as insertable chips in the sidebar, grouped by category. Users can click or drag them into any text/heading block.

<EmailEditor
  variables={[
    { key: 'first_name', sample: 'John', label: 'First Name', group: 'Contact' },
    { key: 'company', sample: 'Acme Inc', label: 'Company', group: 'Contact' },
    { key: 'unsubscribe_url', sample: '#', label: 'Unsubscribe URL', group: 'Links' },
  ]}
/>

| Variable Field | Required | Description | |---------------|----------|-------------| | key | Yes | The placeholder key used in {{ key }} syntax | | sample | No | Sample value shown in previews and tooltips | | label | No | Display label in the sidebar (defaults to key) | | group | No | Group name for organizing variables in the sidebar | | icon | No | Icon shown next to the variable chip |

Retrieving used variables

Extract which variables are actually used in the current template:

const keys = editorRef.current?.getVariables();
// ['first_name', 'unsubscribe_url']

Listening for custom variable changes

Users can create custom variables at runtime via the sidebar. Use onVariablesChange to sync these back to your backend:

<EmailEditor
  variables={backendVariables}
  onVariablesChange={(customVars) => {
    // customVars = variables created by the user in the editor
    saveToBackend(customVars);
  }}
/>

Custom Fonts

Pass custom font families and sizes to the editor. These appear in the rich text toolbar dropdowns.

<EmailEditor
  fontFamilies={[
    'Arial, sans-serif',
    'Georgia, serif',
    'Courier New, monospace',
    'Inter, sans-serif',         // your custom web font
  ]}
  fontSizes={['12px', '14px', '16px', '18px', '20px', '24px', '32px']}
/>

If omitted, the editor falls back to built-in defaults (8 font families, 11 font sizes).

Backend Integration

A complete example showing how to pass placeholders from your backend, save the template, and retrieve used variables:

import { useRef, useEffect, useState } from 'react';
import { EmailEditor } from '@parathantl/react-email-editor';
import '@parathantl/react-email-editor/styles.css';
import type { EmailEditorRef, EmailTemplate, Variable } from '@parathantl/react-email-editor';

function TemplateEditor({ templateId }: { templateId: string }) {
  const editorRef = useRef<EmailEditorRef>(null);
  const [variables, setVariables] = useState<Variable[]>([]);
  const [initialTemplate, setInitialTemplate] = useState<EmailTemplate | undefined>();

  // 1. Load variables and template from backend on mount
  useEffect(() => {
    fetch(`/api/templates/${templateId}`)
      .then((res) => res.json())
      .then((data) => {
        setVariables(data.variables);      // backend-defined placeholders
        setInitialTemplate(data.template); // saved template JSON
      });
  }, [templateId]);

  // 2. Save template + used variables to backend
  const handleSave = async (mjml: string, html: string) => {
    const usedVariables = editorRef.current?.getVariables(); // ['first_name', 'company']
    const templateJSON = editorRef.current?.getJSON();

    await fetch(`/api/templates/${templateId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        mjml,
        html,
        template: templateJSON,
        usedVariables,
      }),
    });
  };

  // 3. Sync user-created custom variables to backend
  const handleVariablesChange = (customVars: Variable[]) => {
    fetch(`/api/templates/${templateId}/variables`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(customVars),
    });
  };

  if (!initialTemplate) return <div>Loading...</div>;

  return (
    <EmailEditor
      ref={editorRef}
      initialTemplate={initialTemplate}
      variables={variables}
      onSave={handleSave}
      onVariablesChange={handleVariablesChange}
      onChange={(template) => {
        // Optional: auto-save on every change
        console.log('Template updated');
      }}
      fontFamilies={['Arial, sans-serif', 'Georgia, serif', 'Inter, sans-serif']}
    />
  );
}

Data flow summary

Backend                          Editor                         Backend
  |                                |                              |
  |-- variables (props) --------> |                              |
  |-- initialTemplate (props) --> |                              |
  |                                |                              |
  |                                | -- onSave(mjml, html) -----> |
  |                                | -- ref.getVariables() -----> |  (used variable keys)
  |                                | -- ref.getJSON() ----------> |  (template structure)
  |                                | -- onVariablesChange() ----> |  (custom variables)

Custom Block Types

Extend the editor with your own block types using the registry:

import {
  registerBlockRenderer,
  registerBlockProperties,
  registerBlockGenerator,
} from '@parathantl/react-email-editor';

// Register a canvas renderer
registerBlockRenderer('my-block', MyBlockComponent);

// Register a properties panel
registerBlockProperties('my-block', MyBlockPropertiesPanel);

// Register an MJML generator
registerBlockGenerator('my-block', (block, indent) => {
  return `${indent}<mj-text>${block.properties.content}</mj-text>`;
});

Theming

The editor uses CSS custom properties scoped under --ee-*. Override them to match your app:

:root {
  --ee-color-primary: #8b5cf6;
  --ee-color-primary-hover: #7c3aed;
  --ee-bg-panel: #fafafa;
  --ee-border-radius: 8px;
  --ee-font-family: 'Inter', sans-serif;
}

See src/styles/variables.css for the full list of 70+ customizable tokens.

Keyboard Shortcuts

| Shortcut | Action | |----------|--------| | Ctrl/Cmd + Z | Undo | | Ctrl/Cmd + Shift + Z / Ctrl/Cmd + Y | Redo | | Ctrl/Cmd + S | Save (triggers onSave) | | Escape | Deselect block/section | | Delete / Backspace | Remove selected block or section |

Responsive Editor

The editor automatically adapts to smaller screens:

  • >= 1024px — Full 3-panel layout (sidebar, canvas, properties)
  • < 1024px — Panels collapse into toggleable overlays with toolbar buttons

Browser Support

Supports all modern browsers (Chrome, Firefox, Safari, Edge). Requires React 18+.

License

MIT