# Email Template Editor
URL: /docs/web/components/email-template-editor
A standalone, beehiiv-style email template editor built on PlateJS. A rich block canvas with a "+"/slash insert palette and merge-tag chips, a full Theme panel (Basic + Advanced) that live-styles every part of the email via CSS variables, email-safe HTML serialization, and a multi-provider preview simulator (Gmail / Outlook / Apple Mail / Mobile / Dark).
**Demo:**
```tsx
'use client';
import {
cloneDefaultTheme,
EmailTemplateEditor,
type EmailDocument
} from '@docyrus/ui/components/email-template-editor';
/*
* Rich sample that exercises the editor-renderable blocks + inlines so every
* Theme-panel control has something visible to affect (headings H1-H6, marks,
* links, bulleted + numbered lists, blockquote, content break, table, merge
* tags). The footer preview reflects the Email Footer theme section.
*/
const SAMPLE_DOC: EmailDocument = {
blocks: [
{
type: 'h1',
children: [{ text: 'Welcome, ' }, { type: 'merge_tag', name: 'first_name', children: [{ text: '' }] }, { text: '!' }]
},
{
type: 'p',
children: [
{ text: 'Your weekly product update — open the ' },
{ text: 'Theme', bold: true },
{ text: ' tab and watch every block restyle live, then check ' },
{ text: 'Preview', bold: true },
{ text: ' to see it across Gmail, Outlook, Apple Mail and mobile.' }
]
},
{
type: 'p',
children: [
{ text: 'Marks: ' },
{ text: 'bold', bold: true },
{ text: ', ' },
{ text: 'italic', italic: true },
{ text: ', ' },
{ text: 'underline', underline: true },
{ text: ', a ' },
{ type: 'a', url: 'https://example.com', children: [{ text: 'themed link' }] },
{ text: '.' }
]
},
{ type: 'h2', children: [{ text: 'This week' }] },
{
type: 'p', indent: 1, listStyleType: 'disc', children: [{ text: 'Faster load times' }]
},
{
type: 'p', indent: 1, listStyleType: 'disc', children: [{ text: 'Redesigned dashboard' }]
},
{
type: 'p', indent: 1, listStyleType: 'disc', children: [{ text: 'New export options' }]
},
{
type: 'blockquote',
children: [{ type: 'p', children: [{ text: 'Design is not just what it looks like. Design is how it works.' }] }]
},
{ type: 'hr', children: [{ text: '' }] },
{
type: 'table',
colSizes: [233, 233, 234],
children: [
{
type: 'tr',
children: [{ type: 'th', children: [{ type: 'p', children: [{ text: 'Plan' }] }] }, { type: 'th', children: [{ type: 'p', children: [{ text: 'Price' }] }] }, { type: 'th', children: [{ type: 'p', children: [{ text: 'Seats' }] }] }]
},
{
type: 'tr',
children: [{ type: 'td', children: [{ type: 'p', children: [{ text: 'Starter' }] }] }, { type: 'td', children: [{ type: 'p', children: [{ text: '$9/mo' }] }] }, { type: 'td', children: [{ type: 'p', children: [{ text: '3' }] }] }]
},
{
type: 'tr',
children: [{ type: 'td', children: [{ type: 'p', children: [{ text: 'Pro' }] }] }, { type: 'td', children: [{ type: 'p', children: [{ text: '$29/mo' }] }] }, { type: 'td', children: [{ type: 'p', children: [{ text: '10' }] }] }]
}
]
},
{ type: 'p', children: [{ text: 'See you next week!' }] }
] as unknown as EmailDocument['blocks'],
theme: cloneDefaultTheme(),
settings: { preheader: 'Your weekly update is here', subject: 'Weekly Update' }
};
export function EmailTemplateEditorDemo() {
return (
{/* The editor owns its own Expand button (top bar) — no external wrapper needed. */}
);
}
```
## Installation
```bash
pnpm dlx @docyrus/cli add @docyrus/ui-email-template-editor
```
**Dependencies:**
- [platejs](https://www.npmjs.com/package/platejs)
## Overview
`EmailTemplateEditor` is a self-contained email authoring surface (no API client or auth required). The top bar has three modes plus two right-docked toggles:
- **Write** — the block canvas: a clean, Notion/beehiiv-style writing column with a rich toolbar (including dedicated **Variables** and **Blocks** insert buttons), a left `+` gutter affordance, a `/` slash menu, and merge-tag chips.
- **Preview** — a multi-provider simulator that renders the **email-safe HTML** across Gmail, Outlook (Word-engine approximation), Apple Mail and Mobile, with a light/dark toggle and a live size badge.
- **Code** — a read-only CodeMirror view of the serialized **email-safe HTML** with an in-panel **Copy HTML** button (mirrors the HTML Template Editor's Code tab).
- **Theme** (right-docked toggle, **not** a mode) — expands / collapses a right-side panel that styles every part of the email (colors, typography, spacing, borders, background, headings, links, lists, blockquote, tables, footer). Changes apply **live** to the canvas and stay open across Write / Preview / Code.
- **Expand** (right-docked toggle) — the editor's own fullscreen overlay; no host wiring needed (hidden in the `dialog` variant, which is already fullscreen).
The document model is a plain, serializable object — `EmailDocument = { blocks, theme, settings? }` — where `blocks` is the Plate value and `theme` is fully decoupled from the content, so persistence is a straight `JSON.stringify`.
## Usage
```tsx
import { useState } from 'react';
import {
cloneDefaultTheme,
EmailTemplateEditor,
serializeEmail,
type EmailDocument
} from '@docyrus/ui/components/email-template-editor';
const initial: EmailDocument = {
blocks: [{ type: 'h1', children: [{ text: 'Welcome, ' }, { type: 'merge_tag', name: 'first_name', children: [{ text: '' }] }, { text: '!' }] }],
theme: cloneDefaultTheme()
};
export function MyEmailEditor() {
const [doc, setDoc] = useState(initial);
return (
console.log(serializeEmail(doc))}
/>
);
}
```
### Mounting modes
`variant="page"` (default) fills its container (`h-full`). `variant="dialog"` opens the same editor inside a fullscreen Radix dialog, controlled via `open` / `onOpenChange`:
```tsx
const [open, setOpen] = useState(false);
```
### Exporting HTML
The **Code** mode's **Copy HTML** button (and the `onExportHtml` callback) return the full **email-safe HTML** — table-based layout, inlined CSS, a Cerberus/goodemailcode reset shell, ``, a hidden preheader, and the themed footer. Call `serializeEmail(doc)` directly to get the same string headlessly.
```tsx
import { serializeEmail } from '@docyrus/ui/components/email-template-editor';
const html = serializeEmail(doc); // literal {{merge_tags}}
const preview = serializeEmail(doc, { preview: true, mergeTags }); // sample values substituted
```
## API Reference
### `EmailTemplateEditorProps`
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `variant` | `'page' \| 'dialog'` | `'page'` | Full-page (fills container) or fullscreen dialog shell |
| `open` | `boolean` | — | Dialog open state (dialog variant, controlled) |
| `onOpenChange` | `(open: boolean) => void` | — | Dialog open-state change handler |
| `defaultValue` | `EmailDocument` | empty doc | Uncontrolled initial document (blocks + theme) |
| `value` | `EmailDocument` | — | Controlled document (optional) |
| `onChange` | `(doc: EmailDocument) => void` | — | Fires on every content or theme change |
| `mergeTags` | `MergeTagDef[]` | `DEFAULT_MERGE_TAGS` | Merge-tag catalog for the `{{ }}` chips + insert menu |
| `onImageUpload` | `(file: File) => Promise` | — | Image upload handler (UI wired; network app-owned) |
| `onExportHtml` | `(html: string) => void` | — | Fires with the email-safe HTML on **Copy HTML** (Code mode) |
| `readOnly` | `boolean` | `false` | Disables editing (canvas becomes read-only) |
| `title` | `string` | `'Email Template Editor'` | Dialog title (dialog variant, screen-reader label) |
| `className` | `string` | — | Class applied to the editor shell |
## Type Exports
| Type | Description |
|------|-------------|
| `EmailDocument` | `{ blocks: Value; theme: EmailTheme; settings?: { preheader?; subject? } }` — the full serializable document |
| `EmailTheme` | The complete theme object (mirrors beehiiv's Basic + Advanced Style panel) |
| `MergeTagDef` | `{ id: string; label: string; group?: string; sample?: string }` — a merge-tag definition |
| `Align` | `'left' \| 'center' \| 'right'` |
| `Spacing` | `number \| { t; r; b; l }` — all-sides or per-side spacing |
| `FontWeight` | `100 … 900` |
| `EmailTemplateEditorProps` | Props for the component |
### `EmailTheme` shape
```ts
interface EmailTheme {
colors: { outsideBackground; postBackground; textOnBackground; primary; textOnPrimary; secondary; links };
typography: { heading: { family; weight }; paragraph: { family; weight } };
spacing: { margin: number; padding: number };
borders: { cornerRadius: number; thickness: number };
background: { color: { canvas; post; postBorder }; margin; padding; radius; borderThickness };
emailHeader: { title; subtitle; image; byline; alignment; padding; code };
body: { text: { family; weight; size; lineHeight; color; gap }; headings: Record<'h1'…'h6', {...}> };
widgets: { links; buttons; breaks; lists; images; tables; blockquote };
emailFooter: { backgroundColor; textColor; family; border; margin; padding; copyright; address; alignment };
}
```
## Exports
| Export | Description |
|--------|-------------|
| `EmailTemplateEditor` | The component |
| `serializeEmail(doc, options?)` | `EmailDocument` → full email-safe HTML string (`{ preview?, mergeTags? }`) |
| `serializeEmailContent(blocks, theme)` | Serialize just the body content (no shell) |
| `themeToCssVars(theme)` | `EmailTheme` → `--et-*` CSS custom-property map (canvas styling path) |
| `cloneDefaultTheme()` / `DEFAULT_EMAIL_THEME` | The default beehiiv-derived theme |
| `DEFAULT_MERGE_TAGS` | The default merge-tag catalog |
| `CLIENT_PROFILES` | The preview render profiles (Gmail / Outlook / Apple Mail / Mobile) |
| `EmailEditorProvider` / `useEmailEditorContext` | Low-level state provider + hook (advanced) |
## Blocks & inserts
Blocks are inserted from the toolbar **Blocks** button (a 3-column palette), the on-canvas left **`+`** gutter (combined blocks + variables), or the **`/`** slash menu. Merge-tag variables are inserted from the toolbar **Variables** button or by typing `{{ field | fallback }}` (auto-converts to a chip).
| Group | Blocks |
|-------|--------|
| Basics | Paragraph, Bulleted / Numbered / To-do list, Blockquote, Code block, Table, Content break (divider), Columns |
| Headings | Heading 1–6 |
| Dynamic | Merge tags (`{{ }}`) |
| Marks | Bold, Italic, Underline, Strikethrough, Inline code, Font color, Alignment, Link |
## Theming engine
One `EmailTheme`, two render targets kept in lockstep:
- **Live canvas** — `themeToCssVars(theme)` produces `--et-*` CSS custom properties applied to the writing surface, so slider/color edits restyle instantly with no re-serialization.
- **Email output** — `serializeEmail` folds the same theme values into **inline styles** on a table-based, client-hardened HTML document.
The **Theme** panel is split into **Basic** (Colors, Typography, Spacing, Borders) and **Advanced** (Background, Email Header, Body H1–H6, Widgets, Email Footer). Basic controls write through to the Advanced fields the renderer reads, so simplified edits still change the output.
## Multi-provider preview
The **Preview** mode renders the serialized email-safe HTML in a sandboxed `