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

email-builder-online

v3.4.49

Published

A React email builder component with drag and drop functionality

Downloads

4,236

Readme

email-builder-online

Powerful, modern email builder with drag-and-drop blocks, live preview, and HTML export. Built with React and Material UI, distributed as a React component and as a Web Component so it can be embedded in other frameworks (Nuxt 3, Next.js, SvelteKit, etc.). Compatible with React 18 and 19.

🚀 Live Demo

Features

  • Drag-and-drop blocks: Text (Rich Text Editor), Heading, Image, Button, Columns, Divider, Spacer, Avatar, HTML, Social Media, Container
  • Editor and Preview tabs with responsive screen sizes (Desktop/Mobile)
  • Undo/Redo and keyboard shortcuts
  • HTML export and copy-to-clipboard helpers
  • CSS size guard for email client compatibility
  • Dark mode support
  • AI features for text (rewrite, grammar, continue, tone) and image generation
  • Image gallery integration with custom image provider support
  • Internationalization (i18n) with English, Spanish, and Italian support
  • Works as React component or Web Component - embed in any framework (Nuxt 3, Next.js, SvelteKit, Vue, etc.)
  • Built with TypeScript for better developer experience
  • Responsive design that works on mobile and desktop

Installation

Install the package and its peer dependencies:

# For React applications
npm i email-builder-online react react-dom react-dnd react-dnd-html5-backend i18next react-i18next i18next-browser-languagedetector
# or
yarn add email-builder-online react react-dom react-dnd react-dnd-html5-backend i18next react-i18next i18next-browser-languagedetector
# or
pnpm add email-builder-online react react-dom react-dnd react-dnd-html5-backend i18next react-i18next i18next-browser-languagedetector

# For Web Component usage (non-React applications)
npm i email-builder-online @r2wc/react-to-web-component i18next react-i18next i18next-browser-languagedetector
# or
yarn add email-builder-online @r2wc/react-to-web-component i18next react-i18next i18next-browser-languagedetector
# or
pnpm add email-builder-online @r2wc/react-to-web-component i18next react-i18next i18next-browser-languagedetector

Note:

  • Material UI (@mui/material) is bundled with the package, so you don't need to install it separately.
  • i18n dependencies (i18next, react-i18next, i18next-browser-languagedetector) are required for internationalization support and must be installed in your project.

Peer dependencies (React / React-DOM)

react and react-dom must be the same major/minor version (e.g. both 18.x or both 19.x). Mixing versions (e.g. react@18 with react-dom@19) can cause runtime errors such as "Cannot read properties of undefined (reading 'S')".

If your project uses Vite, add this to your vite.config to avoid multiple instances of React (recommended on Windows and in monorepos):

export default defineConfig({
  resolve: {
    dedupe: ['react', 'react-dom', 'react-dom/client'],
  },
  // ...
});

Add the stylesheet (required):

// React / Vite / Nuxt / Next
import 'email-builder-online/style.css';

AI Features

The builder supports AI-powered features for both text and image blocks, controlled by a single enableAI prop.

Text AI

When enableAI is true, the rich text editor (NotionText) shows AI actions in the bubble menu toolbar and slash menu:

  • Rewrite, grammar check, continue writing
  • Tone adjustments: shorter, descriptive, detailed, friendly, professional

Text AI uses the onAIRequest callback to process requests. You provide the backend integration:

<EmailBuilder
  enableAI={true}
  onAIRequest={async ({ text, content, action, blockId }) => {
    const response = await fetch('/api/ai', {
      method: 'POST',
      body: JSON.stringify({ text, content, action }),
      headers: { 'Content-Type': 'application/json' },
    });
    const data = await response.json();
    return data.processedContent;
  }}
/>

Image AI Generation

When enableAI is true, image blocks and background image inputs show a "Generate with AI" button that opens a dialog for prompt-based image generation.

Events

The image AI generation uses a custom event system:

request-ai-image - Fired when the user submits a prompt to generate an image

window.addEventListener('request-ai-image', (event: CustomEvent) => {
  const prompt = event.detail; // string: the user's prompt

  // Call your AI image generation API
  const imageUrl = await generateImage(prompt);

  // Respond with the generated image
  window.dispatchEvent(
    new CustomEvent('generated-image', {
      detail: { url: imageUrl, success: true },
    })
  );
});

store-ai-image - Fired when the user confirms and inserts the generated image

window.addEventListener('store-ai-image', (event: CustomEvent) => {
  const imageUrl = event.detail; // string: URL of the image to store
  // Persist the image to your storage if needed
});

generated-image - Dispatch this event to return the generated image to the builder

// Success
window.dispatchEvent(
  new CustomEvent('generated-image', {
    detail: { url: 'https://example.com/generated.png', success: true },
  })
);

// Error
window.dispatchEvent(
  new CustomEvent('generated-image', {
    detail: { url: null, success: false, error: { code: 500, message: 'Generation failed' } },
  })
);

Custom Image Provider

You can integrate your own image selector/gallery by passing a React component through the customImageProvider prop. This component will be rendered at the top of the image input panel, allowing users to select images from your custom source.

Events

The custom image provider can listen to and dispatch the following events:

Listening to Events

email-builder-image-panel-opened - Fired when an Image block is selected/opened in the editor

window.addEventListener('email-builder-image-panel-opened', (event: CustomEvent) => {
  const { blockId, currentImageUrl, alt } = event.detail;
  // You can use this to highlight the current image in your gallery
  // or load additional data based on the current selection
});

Event detail properties:

  • blockId (string): The ID of the selected image block
  • currentImageUrl (string | null): The URL of the currently selected image, or null if no image is set
  • alt (string | null): The alt text of the current image, or null if not set

Dispatching Events

email-builder-set-image - Dispatch this event to set an image in the currently selected block

window.dispatchEvent(
  new CustomEvent('email-builder-set-image', {
    detail: imageUrl, // string: URL of the image to set
  })
);

Complete Example

import React, { useEffect, useState } from 'react';
import { EmailBuilder } from 'email-builder-online';

function MyImageGallery() {
  const [currentImageUrl, setCurrentImageUrl] = useState<string | null>(null);

  useEffect(() => {
    // Listen for when the image panel opens
    const handlePanelOpened = (event: CustomEvent) => {
      const { currentImageUrl } = event.detail;
      setCurrentImageUrl(currentImageUrl);
    };

    window.addEventListener('email-builder-image-panel-opened', handlePanelOpened);

    return () => {
      window.removeEventListener('email-builder-image-panel-opened', handlePanelOpened);
    };
  }, []);

  const handleImageSelect = (imageUrl: string) => {
    // Dispatch event to set the image
    window.dispatchEvent(
      new CustomEvent('email-builder-set-image', {
        detail: imageUrl,
      })
    );
  };

  return (
    <div>
      <h4>My Custom Gallery</h4>
      {currentImageUrl && <p style={{ fontSize: '12px', color: '#666' }}>Current: {currentImageUrl}</p>}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
        <img
          src="https://example.com/image1.jpg"
          onClick={() => handleImageSelect('https://example.com/image1.jpg')}
          style={{
            cursor: 'pointer',
            width: '100%',
            border: currentImageUrl === 'https://example.com/image1.jpg' ? '2px solid blue' : 'none',
          }}
        />
        {/* More images... */}
      </div>
    </div>
  );
}

export default function Page() {
  return (
    <EmailBuilder
      customImageProvider={<MyImageGallery />}
      // ... other props
    />
  );
}

Live Example: Check out the working implementation in packages/editor-sample/src/components/SampleImageGallery.tsx and see it in action in the demo app.

Custom Merge Tags

You can provide your own merge tags that will appear in the text editor's merge tag menu. Pass an array of tags or a complete group configuration:

Simple Array of Tags

import { EmailBuilder, MergeTag } from 'email-builder-online';

const myMergeTags: MergeTag[] = [
  {
    label: 'First Name',
    value: '[first_name]',
  },
  {
    label: 'Last Name',
    value: '[last_name]',
  },
  {
    label: 'Company',
    value: '[company]',
  },
  {
    type: 'divider', // Add a divider
  },
  {
    label: 'Custom Link',
    value: '{custom_link}Click here{/custom_link}',
  },
];

export default function Page() {
  return (
    <EmailBuilder
      mergeTags={myMergeTags}
      // ... other props
    />
  );
}

Complete Group with Custom Icons

import { EmailBuilder, MergeTagGroup } from 'email-builder-online';
import { User, Building, Mail } from 'lucide-react'; // or any icon library

const myMergeTagGroup: MergeTagGroup = {
  label: 'My Custom Tags',
  icon: <Mail />,
  children: [
    {
      label: 'First Name',
      value: '[first_name]',
      icon: <User />,
    },
    {
      label: 'Company Name',
      value: '[company]',
      icon: <Building />,
    },
    {
      type: 'divider',
    },
    {
      label: 'Verification Link',
      value: '{verify}Verify Account{/verify}',
      icon: <Mail />,
    },
  ],
};

export default function Page() {
  return (
    <EmailBuilder
      mergeTags={myMergeTagGroup}
      // ... other props
    />
  );
}

Programmatic Image Loading

You can load images programmatically using the ref:

import { useRef } from 'react';
import { EmailBuilder, EmailBuilderRef } from 'email-builder-online';

export default function Page() {
  const emailBuilderRef = useRef<EmailBuilderRef>(null);

  const handleSelectFromMyGallery = (imageUrl: string, blockId: string) => {
    // Set image URL for a specific block
    emailBuilderRef.current?.setImageUrl(blockId, imageUrl);
  };

  return (
    <EmailBuilder
      ref={emailBuilderRef}
      customImageProvider={<MyCustomGallery onSelect={handleSelectFromMyGallery} />}
    />
  );
}

Using with Custom Image Provider Events

You can combine the ref approach with the event system for a more robust solution:

import { useRef, useEffect, useState } from 'react';
import { EmailBuilder, EmailBuilderRef } from 'email-builder-online';

function MyCustomGallery() {
  const [currentBlockId, setCurrentBlockId] = useState<string | null>(null);
  const [currentImageUrl, setCurrentImageUrl] = useState<string | null>(null);

  useEffect(() => {
    const handlePanelOpened = (event: CustomEvent) => {
      const { blockId, currentImageUrl } = event.detail;
      setCurrentBlockId(blockId);
      setCurrentImageUrl(currentImageUrl);
    };

    window.addEventListener('email-builder-image-panel-opened', handlePanelOpened);
    return () => window.removeEventListener('email-builder-image-panel-opened', handlePanelOpened);
  }, []);

  const handleImageSelect = (imageUrl: string) => {
    // Use the event system to set the image
    window.dispatchEvent(
      new CustomEvent('email-builder-set-image', {
        detail: imageUrl,
      })
    );
  };

  return (
    <div>
      <h4>Select Image</h4>
      {currentImageUrl && <p>Current: {currentImageUrl}</p>}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
        {/* Your image gallery */}
      </div>
    </div>
  );
}

export default function Page() {
  const emailBuilderRef = useRef<EmailBuilderRef>(null);

  // Alternative: Use ref method directly
  const handleSelectFromGallery = (imageUrl: string, blockId: string) => {
    emailBuilderRef.current?.setImageUrl(blockId, imageUrl);
  };

  return <EmailBuilder ref={emailBuilderRef} customImageProvider={<MyCustomGallery />} />;
}

Available Blocks

The email builder includes the following drag-and-drop blocks:

  • Text - Rich text editor with formatting options (uses CustomEditor)
  • Notion Text - Advanced rich text editor with Notion-like editing experience
  • WYSIWYG - What-You-See-Is-What-You-Get rich text editor
  • Heading - Heading block for titles and subtitles
  • Image - Image block with link support
  • Button - Call-to-action button with customizable styling
  • Columns - Multi-column layout container
  • Divider - Horizontal divider/separator line
  • Spacer - Vertical spacing block
  • Avatar - Profile picture/avatar image
  • HTML - Raw HTML block for custom code
  • Social Media - Social media icons with links
  • Container - Container block for grouping content

Backward Compatibility

The email builder automatically migrates legacy blocks to ensure compatibility with older templates:

  • CustomEditor blocks → Automatically converted to NotionText
  • Wysiwyg blocks → Automatically converted to NotionText

This migration happens automatically when loading a document, so templates created before the introduction of the NotionText block will continue to work seamlessly. The migration preserves all content and styling while enabling the new editing features.

Usage

There are three ways to use the builder:

1) As a React component with Ref (recommended for programmatic control)

Use a ref to control saving and access the document programmatically:

import React, { useRef, useState, useEffect } from 'react';
import { EmailBuilder, EmailBuilderRef, TEditorConfiguration, MergeTag } from 'email-builder-online';
import 'email-builder-online/style.css';

// Custom Image Gallery Component
function MyImageGallery() {
  const images = ['https://example.com/image1.jpg', 'https://example.com/image2.jpg', 'https://example.com/image3.jpg'];

  const handleImageSelect = (imageUrl: string) => {
    window.dispatchEvent(
      new CustomEvent('email-builder-set-image', {
        detail: imageUrl,
      })
    );
  };

  return (
    <div>
      <h4>My Custom Gallery</h4>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
        {images.map((img, idx) => (
          <img
            key={idx}
            src={img}
            onClick={() => handleImageSelect(img)}
            style={{ cursor: 'pointer', width: '100%', borderRadius: '4px' }}
          />
        ))}
      </div>
    </div>
  );
}

// Custom Merge Tags
const customMergeTags: MergeTag[] = [
  {
    label: 'First Name',
    value: '[first_name]',
  },
  {
    label: 'Last Name',
    value: '[last_name]',
  },
  {
    label: 'Company',
    value: '[company]',
  },
  {
    type: 'divider',
  },
  {
    label: 'Verification Link',
    value: '{verify}Verify your account{/verify}',
  },
];

export default function Page() {
  const emailBuilderRef = useRef<EmailBuilderRef>(null);
  const [document, setDocument] = useState<TEditorConfiguration | null>(null);

  // Load saved document on mount
  useEffect(() => {
    const saved = localStorage.getItem('draft');
    if (saved) {
      setDocument(JSON.parse(saved));
    }
  }, []);

  const handleSave = () => {
    if (emailBuilderRef.current) {
      const document = emailBuilderRef.current.save();
      // Send to your backend
      fetch('/api/save', {
        method: 'POST',
        body: JSON.stringify(document),
        headers: { 'Content-Type': 'application/json' },
      });
    }
  };

  const handleExportHtml = () => {
    if (emailBuilderRef.current) {
      const html = emailBuilderRef.current.getHtml();
      // Download HTML file or send to backend
      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();
      URL.revokeObjectURL(url);
    }
  };

  const handleSendEmail = async () => {
    if (emailBuilderRef.current) {
      const html = emailBuilderRef.current.getHtml();
      // Send email via your backend
      await fetch('/api/send-email', {
        method: 'POST',
        body: JSON.stringify({ html }),
        headers: { 'Content-Type': 'application/json' },
      });
    }
  };

  return (
    <div style={{ height: '100vh' }}>
      <button onClick={handleSave}>Save</button>
      <button onClick={handleExportHtml}>Export HTML</button>
      <button onClick={handleSendEmail}>Send Email</button>
      <EmailBuilder
        ref={emailBuilderRef}
        data={document}
        onAutoSave={(doc) => localStorage.setItem('draft', JSON.stringify(doc))}
        customImageProvider={<MyImageGallery />}
        mergeTags={customMergeTags}
        primaryColor="#0d9488"
        secondaryColor="#0ea5a6"
        locale="en"
        height="calc(100vh - 80px)"
      />
    </div>
  );
}

Ref Methods:

The EmailBuilderRef exposes the following methods:

  • getDocument(): TEditorConfiguration - Returns the current editor document
  • setDocument(document: TEditorConfiguration): void - Replaces the current document
  • save(): TEditorConfiguration - Flushes pending changes, calls onSave (if provided) and returns the document
  • getHtml(): string - Returns the rendered HTML for the current document
  • setImageUrl(blockId: string, url: string): void - Sets the URL of an image block with the given ID and updates the document

2) As a React component (basic usage)

import React from 'react';
import { EmailBuilder } from 'email-builder-online';
import 'email-builder-online/style.css';

export default function Page() {
  return (
    <div style={{ height: '100vh' }}>
      <EmailBuilder
        primaryColor="#0d9488"
        secondaryColor="#0ea5a6"
        darkMode={false}
        stickyHeader
        locale="en"
        height="calc(100vh - 80px)"
      />
    </div>
  );
}

3) As a Web Component (works in Nuxt 3, Vue, Svelte, plain HTML)

For Web Component usage, the component auto-registers in browser environments. Here's how to use it in different frameworks:

Nuxt 3 Example

  1. Register the email builder component:
// plugins/email-builder.client.ts
import { registerEmailBuilder } from 'email-builder-online';
import 'email-builder-online/style.css';

export default defineNuxtPlugin(() => {
  // Register the web component
  registerEmailBuilder('email-builder');
});

Vanilla JavaScript Example

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="path/to/email-builder-online/style.css" />
    <script src="path/to/email-builder-online/dist/index.umd.js"></script>
  </head>
  <body>
    <script>
      // Register the web component
      window.EmailBuilder.registerEmailBuilder('email-builder');
    </script>

    <email-builder></email-builder>
  </body>
</html>

Then use it anywhere in your templates (wrap in <ClientOnly> for Nuxt):

<template>
  <ClientOnly>
    <email-builder
      primary-color="#0d9488"
      secondary-color="#0ea5a6"
      dark-mode="false"
      sticky-header="true"
      locale="en"
      height="calc(100vh - 80px)"
    />
  </ClientOnly>
</template>

For Vue/Nuxt, you can silence unknown element warnings by marking the tag as a custom element:

// nuxt.config.ts
export default defineNuxtConfig({
  vue: {
    compilerOptions: {
      isCustomElement: (tag) => tag === 'email-builder',
    },
  },
  css: ['email-builder-online/style.css'],
});

Notes on SSR and dependencies:

  • The Web Component wrapper is registered only on the client. Use a client-only plugin in SSR frameworks.
  • React and ReactDOM are peer dependencies even for the Web Component. Install them in your app or use a bundler that provides them. React 18 and 19 are supported.
  • i18n dependencies (i18next, react-i18next, i18next-browser-languagedetector) are required for internationalization support and must be installed in your project.

Custom Editor Standalone

The package also ships the CustomEditorInputStandalone component so the rich text block can be embedded outside of the full builder experience.

React

import { useState } from 'react';
import { CustomEditorInputStandalone } from 'email-builder-online';
import 'email-builder-online/style.css';

export default function CustomEditorExample() {
  const [value, setValue] = useState('<p>Write your custom content here…</p>');

  return (
    <CustomEditorInputStandalone
      initialData={value}
      onChange={setValue}
      editorBackgroundColor="#ffffff"
      editorColorDefault="#1f2937"
      fontFamily="MODERN_SANS"
      lineHeight={1.6}
    />
  );
}

Web Component

import { registerCustomEditorInput } from 'email-builder-online';
import 'email-builder-online/style.css';

if (typeof window !== 'undefined') {
  registerCustomEditorInput('custom-editor'); // default is "custom-editor-input"
}

Props / Attributes

React Props (camelCase) and Web Component attributes (dash-case) map 1:1:

| React Prop | Web Component Attribute | Type | Default | Description | | ------------------- | ----------------------- | ---------------------------------------- | ------- | ---------------------------------------------------------------------------------------- | | ref | - | React.Ref | - | Ref to access component methods (getDocument, setDocument, save, getHtml, setImageUrl) | | onSave | - | (document: TEditorConfiguration) => void | - | Callback when ref.save() is called | | onAutoSave | - | (document: TEditorConfiguration) => void | - | Callback for auto-save (2s after changes) | | data | data | TEditorConfiguration | string | - | Document to load (reactive - updates when changed) | | initialDocument | - | TEditorConfiguration | string | - | Initial document to load on mount (one-time only) | | customImageProvider | - | React.ReactNode | - | Custom image selector component to integrate your own image gallery | | mergeTags | - | MergeTag[] | MergeTagGroup | - | Custom merge tags to show in the text editor | | primaryColor | primary-color | string | #058705 | Primary theme color | | secondaryColor | secondary-color | string | #079707 | Secondary theme color | | darkMode | dark-mode | boolean | false | Enable dark mode | | height | height | string | - | Container height (e.g. "calc(100vh - 80px)") | | stickyHeader | sticky-header | boolean | true | Sticky header behavior | | sticky | sticky | boolean | false | Sticky content behavior | | galleryImages | gallery-images | boolean | false | Enable image gallery | | locale | locale | string | - | UI language (en, es, it, en-US, es-419, it-IT). Falls back to dataLocale if not provided | | dataLocale | data-locale | string | - | Alternative locale prop (used as fallback if locale is not provided) | | htmlTab | html-tab | boolean | true | Show HTML tab | | jsonTab | json-tab | boolean | true | Show JSON tab | | imagePlaceholder | image-placeholder | string | - | Default placeholder for images | | enableAI | enable-ai | boolean | false | Enable AI features for text and image generation | | onAIRequest | - | (request: AIFeatureRequest) => Promise<string> | - | Callback for AI text processing requests | | freeMode | free-mode | boolean | true | Enable free editing mode | | showVersion | show-version | boolean | - | Show version indicator in the editor | | componentTree | component-tree | boolean | true | Show the component tree panel |

Internationalization (i18n)

The builder supports multiple languages. Pass the locale prop with one of the supported values:

  • en or en-US - English (default)
  • es or es-419 - Spanish
  • it or it-IT - Italian
<EmailBuilder locale="es" />

TypeScript

Types are shipped. You can import them as:

import type { EmailBuilderProps, AIFeatureRequest, MergeTag, MergeTagGroup, EmailBuilderRef } from 'email-builder-online';

The AIFeatureRequest interface:

interface AIFeatureRequest {
  text: string;      // Selected or relevant text
  content: string;   // Full block content
  action: string;    // AI action (rewrite, grammar_check, continue_writing, shorter, descriptive, detailed, friendly, professional)
  blockId?: string;  // Block ID where the request originated
}

License

MIT © Laravel42

Links

Changelog

See Git history for details. Please open issues or PRs for bugs and improvements.