Skip to main content

Overview

The Email Builder example demonstrates how to create an email composition tool with templates, signatures, split-view editing, HTML preview, and email-safe export.

Live Demo

Try the interactive email builder demo

Features

Email Templates

Pre-built templates for common email types

Split View

Edit and preview side-by-side

HTML Preview

See the generated email HTML in real-time

Email-Safe Export

Export with inline styles for email clients

Implementation

Email Templates

Pre-built templates for different email types:
const EMAIL_TEMPLATES = [
  {
    id: 'blank',
    name: 'Blank Email',
    subject: '',
    content: { /* Empty Yoopta value */ },
  },
  {
    id: 'welcome',
    name: 'Welcome Email',
    subject: 'Welcome to Our Platform!',
    content: {
      'block-1': {
        type: 'Paragraph',
        value: [{
          type: 'paragraph',
          children: [{ text: 'Hi there,' }],
        }],
      },
      'block-2': {
        type: 'Paragraph',
        value: [{
          type: 'paragraph',
          children: [
            { text: 'Welcome to ' },
            { text: 'Our Platform', bold: true },
            { text: '! We\'re excited to have you on board.' },
          ],
        }],
      },
      // More blocks...
    },
  },
  {
    id: 'newsletter',
    name: 'Newsletter',
    subject: 'Monthly Newsletter - [Month]',
    content: { /* Newsletter template */ },
  },
];

Email Builder Component

import { useState, useMemo } from 'react';
import YooptaEditor, { createYooptaEditor } from '@yoopta/editor';
import { EMAIL_PLUGINS, EMAIL_MARKS } from './config';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Send, Eye, Code } from 'lucide-react';

function EmailBuilder() {
  const [subject, setSubject] = useState('');
  const [to, setTo] = useState('');
  const [viewMode, setViewMode] = useState<'edit' | 'preview' | 'html'>('edit');
  
  const editor = useMemo(() => {
    return createYooptaEditor({
      plugins: EMAIL_PLUGINS,
      marks: EMAIL_MARKS,
    });
  }, []);

  return (
    <div className="h-screen flex flex-col">
      {/* Email Header */}
      <div className="border-b p-4 space-y-3">
        <div className="flex gap-2">
          <Input
            placeholder="To:"
            value={to}
            onChange={(e) => setTo(e.target.value)}
          />
          <Button onClick={handleSend}>
            <Send className="h-4 w-4 mr-2" />
            Send
          </Button>
        </div>
        
        <Input
          placeholder="Subject:"
          value={subject}
          onChange={(e) => setSubject(e.target.value)}
        />
      </div>

      {/* Toolbar */}
      <div className="border-b p-2 flex gap-2">
        <Button
          size="sm"
          variant={viewMode === 'edit' ? 'default' : 'outline'}
          onClick={() => setViewMode('edit')}
        >
          Edit
        </Button>
        <Button
          size="sm"
          variant={viewMode === 'preview' ? 'default' : 'outline'}
          onClick={() => setViewMode('preview')}
        >
          <Eye className="h-4 w-4 mr-2" />
          Preview
        </Button>
        <Button
          size="sm"
          variant={viewMode === 'html' ? 'default' : 'outline'}
          onClick={() => setViewMode('html')}
        >
          <Code className="h-4 w-4 mr-2" />
          HTML
        </Button>
      </div>

      {/* Editor Area */}
      <div className="flex-1 overflow-auto">
        {viewMode === 'edit' && (
          <div className="max-w-3xl mx-auto p-8">
            <YooptaEditor
              editor={editor}
              placeholder="Compose your email..."
            />
          </div>
        )}
        
        {viewMode === 'preview' && (
          <EmailPreview content={editor.getEmail()} />
        )}
        
        {viewMode === 'html' && (
          <HTMLPreview html={editor.getEmail()} />
        )}
      </div>
    </div>
  );
}

Email-Safe Plugins

Limit to email-compatible features:
import Paragraph from '@yoopta/paragraph';
import Headings from '@yoopta/headings';
import Lists from '@yoopta/lists';
import Blockquote from '@yoopta/blockquote';
import Image from '@yoopta/image';
import Link from '@yoopta/link';
import Divider from '@yoopta/divider';
import { Bold, Italic, Underline, Strike } from '@yoopta/marks';

export const EMAIL_PLUGINS = [
  Paragraph,
  Headings.HeadingOne,
  Headings.HeadingTwo,
  Headings.HeadingThree,
  Lists.BulletedList,
  Lists.NumberedList,
  Blockquote,
  Image.extend({
    options: {
      // Ensure images are hosted and accessible
      async onUpload(file) {
        const url = await uploadToEmailCDN(file);
        return { src: url };
      },
    },
  }),
  Link,
  Divider,
];

export const EMAIL_MARKS = [Bold, Italic, Underline, Strike];

Template Selector

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { FileText } from 'lucide-react';

function TemplateSelector({ onSelect }: { onSelect: (template: Template) => void }) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button size="sm" variant="outline">
          <FileText className="h-4 w-4 mr-2" />
          Templates
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        {EMAIL_TEMPLATES.map((template) => (
          <DropdownMenuItem
            key={template.id}
            onClick={() => onSelect(template)}
          >
            {template.name}
          </DropdownMenuItem>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

Email Export

Export with inline styles for email clients:
import { useYooptaEditor } from '@yoopta/editor';

function useEmailExport() {
  const editor = useYooptaEditor();

  const exportEmail = () => {
    // Get email-safe HTML with inline styles
    const html = editor.getEmail();
    
    // Wrap in email template
    const emailHTML = `
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${subject}</title>
</head>
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif;">
  <table width="100%" cellpadding="0" cellspacing="0" style="max-width: 600px; margin: 0 auto;">
    <tr>
      <td style="padding: 20px;">
        ${html}
      </td>
    </tr>
  </table>
</body>
</html>
    `;
    
    return emailHTML;
  };

  return { exportEmail };
}

HTML Preview Component

import { ScrollArea } from '@/components/ui/scroll-area';
import { Button } from '@/components/ui/button';
import { Copy, Check } from 'lucide-react';
import { useState } from 'react';

function HTMLPreview({ html }: { html: string }) {
  const [copied, setCopied] = useState(false);

  const handleCopy = async () => {
    await navigator.clipboard.writeText(html);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };

  return (
    <div className="p-4">
      <div className="flex justify-between items-center mb-4">
        <h3 className="font-semibold">Email HTML</h3>
        <Button size="sm" onClick={handleCopy}>
          {copied ? (
            <><Check className="h-4 w-4 mr-2" /> Copied!</>
          ) : (
            <><Copy className="h-4 w-4 mr-2" /> Copy HTML</>
          )}
        </Button>
      </div>
      
      <ScrollArea className="h-[600px] rounded border">
        <pre className="p-4 text-xs">
          <code>{html}</code>
        </pre>
      </ScrollArea>
    </div>
  );
}

Email Preview Component

function EmailPreview({ content }: { content: string }) {
  return (
    <div className="max-w-3xl mx-auto p-8">
      <div className="border rounded-lg overflow-hidden">
        {/* Email header */}
        <div className="bg-neutral-100 p-4 border-b">
          <div className="text-sm space-y-1">
            <div><strong>From:</strong> [email protected]</div>
            <div><strong>To:</strong> {to}</div>
            <div><strong>Subject:</strong> {subject}</div>
          </div>
        </div>
        
        {/* Email body */}
        <div
          className="p-8 bg-white"
          dangerouslySetInnerHTML={{ __html: content }}
        />
      </div>
    </div>
  );
}

Send Email Function

function useSendEmail() {
  const editor = useYooptaEditor();

  const sendEmail = async ({
    to,
    subject,
  }: {
    to: string;
    subject: string;
  }) => {
    const html = editor.getEmail();
    
    const response = await fetch('/api/send-email', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        to,
        subject,
        html,
      }),
    });
    
    if (!response.ok) {
      throw new Error('Failed to send email');
    }
    
    return response.json();
  };

  return { sendEmail };
}

Email Compatibility

Email clients have limited CSS support. The getEmail() method exports HTML with inline styles for maximum compatibility.

Supported Features

  • Basic text formatting (bold, italic, underline)
  • Headings and paragraphs
  • Lists (bulleted and numbered)
  • Images (must be hosted)
  • Links
  • Dividers
  • Custom fonts (use web-safe fonts)
  • Complex layouts (use tables)
  • Advanced CSS (flexbox, grid)
  • JavaScript
  • Video embeds

Source Code

View Full Source

Complete email builder implementation on GitHub

Use Cases

Email Campaigns

Create marketing emails with templates

Newsletters

Design and send company newsletters

Transactional Emails

Build order confirmations and notifications

Email Signatures

Create rich email signatures

Next Steps

README Editor

Learn about the Markdown editor

Exports

Explore all export options

Build docs developers (and LLMs) love