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

@kontakto/email-template-editor

v2.13.0

Published

A React-based email template editor component that allows users to create and customize email templates through a visual interface. This component can be embedded in any React application.

Readme

Email Editor for React

A React-based email template editor component that allows users to create and customize email templates through a visual interface. This component can be embedded in any React application.

Features

  • Visual email template builder
  • Rich set of components (text, buttons, images, dividers, containers, columns, etc.)
  • Markdown support for text editing
  • Responsive email templates
  • Embeddable into React applications

Installation

npm install @kontakto/email-template-editor

Usage

Embedding into existing project

You can easily embed the EmailEditor into any React application:

import { EmailEditor } from '@kontakto/email-template-editor';

function MyApp() {
  return (
    <div className="my-container">
      <EmailEditor 
        persistenceEnabled={true}
        minHeight="80vh"
      />
    </div>
  );
}

Available Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | minHeight | string | '100vh' | Sets the minimum height of the editor container | | persistenceEnabled | boolean | false | When true, enables save functionality and shows save buttons. Requires callbacks for persistence operations. | | samplesDrawerEnabled | boolean | true | Controls whether the templates/samples drawer is shown | | drawerEnterDuration | number | 225 | Duration for drawer enter transition (ms) | | drawerExitDuration | number | 225 | Duration for drawer exit transition (ms) | | initialTemplate | object | - | Initial template to load when editor first mounts | | initialTemplateId | string | - | ID of the initial template | | initialTemplateName | string | - | Name of the initial template | | onSave | function | - | Callback when template is saved: (payload: SavePayload) => void \| Promise<void> (see SavePayload below) | | onChange | function | - | Callback when template changes: (template) => void | | loadSamples | function | - | Loads sample templates: () => Promise<TemplateListItem[]> | | loadTemplates | function | - | Loads user templates: () => Promise<TemplateListItem[]> | | loadTemplate | function | - | Loads specific template: (id) => Promise<Template> | | deleteTemplate | function | - | Deletes a template: (id) => void | | copyTemplate | function | - | Copies a template: (name, content) => void | | renameTemplate | function | - | Renames a template: (id, newSlug) => void \| Promise<void> | | setTemplateKind | function | - | Promotes/demotes a row between template and sample: (id, kind) => void \| Promise<void>. When omitted, promote/demote menu items are hidden. | | saveAs | function | - | Saves template with a new name: (name, payload: SavePayload) => Promise<{ id, slug }> | | uploadImage | function | - | Uploads a single image file: (file: File) => Promise<UploadedImage>. Enables the Upload button on the Image inspector, drag-and-drop on the canvas, and paste-image-to-insert. When omitted, all upload UI is hidden and URL paste remains the only way to set an image. | | loadImages | function | - | Lists previously uploaded images for the library picker: () => Promise<LibraryImage[]>. Enables the "Library" button on the Image inspector. When omitted, the library button is hidden. | | deleteImage | function | - | Deletes an image from the library by URL: (url: string) => Promise<void>. When omitted, the per-row delete button in the library is hidden. |

TemplateListItem is the lean list-endpoint shape (no editor_config):

type TemplateKind = 'template' | 'sample';

type TemplateListItem = {
  id: string;
  slug: string;                 // primary label
  kind: TemplateKind;           // 'template' (editable) or 'sample' (read-only starting point)
  description?: string;         // secondary line
  subject?: string;
  variables?: Array<{ name: string; description?: string }>;
  tags?: string[];
  thumbnailUrl?: string;
  createdAt?: string;
  updatedAt?: string;
};

The drawer groups rows by kind, not by which callback returned them. Both loadTemplates and loadSamples should return their items with the correct kind; backends typically scope the two endpoints differently (per-user vs. org-wide), but the kind field is what determines the section a row appears in.

Samples are read-only starting points: Save on a loaded sample is disabled — the user must use Save As, which creates a fresh row with kind='template'.

Subject and variables

Email subject and template variables are stored on the EmailLayout block's data and round-trip with the editor configuration:

type EmailLayoutData = {
  // ...style fields
  subject?: string;
  variables?: Array<{ name: string; description?: string; sampleValue?: string }>;
};

The editor renders a subject input above the canvas (always visible, supports {{variable}} syntax) and a Variables tab in the right inspector panel for declaring variables. Both persist via the standard save flow — consumers who previously stored subject in a separate DB column can read it from the saved editor_config instead.

The Variables tab supports Handlebars-aware management:

  • Add/rename/delete. Names follow Handlebars identifier rules ([A-Za-z_][A-Za-z0-9_]*, max 64 chars, reserved words rejected). Renaming a declared variable rewrites all {{oldName}} and {{oldName.*}} tokens in the subject and in text/heading/button/html blocks — including inside block helpers ({{#if}}, {{#each}}, {{#unless}}, {{#with}}).
  • Usage indicators. Each row shows how many times the variable is referenced, or "Unused in body" if the declared name never appears. Tokens found in the body that aren't declared surface at the top of the panel with a one-click "add as variable" action.
  • Insert at cursor. Focus a text/heading/button/html editor or the subject input, then click the Insert button next to a variable to splice {{name}} at the caret.
  • Sample values. Each row has an optional sampleValue field that travels with the template (persisted on editor_config.root.data.variables[].sampleValue). In Preview mode, {{name}} and {{name.*}} tokens in the subject and in text/heading/button/html blocks render with the sample value substituted in; block helpers ({{#if}}, {{#each}}, …) are stripped so their content renders inline, but the control flow is not evaluated. Edit mode always shows the raw tokens.

Save payload

onSave and saveAs receive the same SavePayload. The editor renders body HTML and plain text on every save so consumers don't ship the renderer themselves:

type SavePayload = {
  editorConfig: TEditorConfiguration;   // source of truth
  subject?: string;                     // from the subject input
  variables?: Array<{ name: string; description?: string }>;
  bodyHtml: string;                     // pre-rendered, ready to send
  bodyText: string;                     // pre-rendered, ready to send
};

The renderToStaticMarkup and renderToText utilities are also exposed publicly for consumers that need to re-render outside the save flow (e.g. batch jobs).

Handlebars render pipeline

renderToStaticMarkup(doc, { rootBlockId, variables? }) and renderToText(doc, { rootBlockId, variables? }) both accept an optional variables context. When omitted (the default — including buildSavePayload), the output keeps raw {{name}} placeholders in place so the consumer's sending pipeline can evaluate them at send time with the recipient's actual data. When provided, the rendered output is compiled and evaluated as a Handlebars template:

renderToStaticMarkup(doc, {
  rootBlockId: 'root',
  variables: { name: 'Alice', premium: true, amount: 19.99, next_bill: '2026-05-01' },
});
// → "<!DOCTYPE html>...Hi Alice! Premium user. Next bill: 5/1/2026, total €19.99..."

Built-in block helpers are whatever Handlebars ships with: {{#if}}, {{#unless}}, {{#each}}, {{#with}}, {{else}}. The editor registers two extra formatters on a scoped Handlebars instance:

  • {{formatDate value "date"|"time"|"datetime"|"iso"}} — formats a Date or ISO string via the user's locale.
  • {{formatNumber value currency="EUR" style="currency" maximumFractionDigits=0}} — wraps Intl.NumberFormat.

The scoped instance is exported as editorHandlebars for consumers who want to register additional helpers or partials without polluting the global Handlebars default instance:

import { editorHandlebars, evaluateHandlebars } from '@kontakto/email-template-editor';

editorHandlebars.registerHelper('upper', (s: string) => (s ?? '').toUpperCase());
evaluateHandlebars('Hello {{upper name}}', { name: 'world' }); // → "Hello WORLD"

Backwards compat: templates using only simple {{placeholder}} substitution render identically whether Handlebars is invoked or not, so existing consumers can opt in gradually.

Image upload and library (BYO backend)

The editor delegates image storage to the consumer through three optional callbacks. When omitted, the corresponding UI is hidden and URL paste remains the fallback.

type UploadedImage = {
  url: string;
  width?: number;
  height?: number;
  alt?: string;
};

type LibraryImage = UploadedImage & {
  thumbnailUrl?: string;
  uploadedAt?: string;
};
  • uploadImage(file) — receives a single File, uploads it (S3, R2, Bunny, presigned PUT, etc.), and returns the public url plus optional intrinsic dimensions and alt text. Wires up: the Upload button in the Image inspector, drag-and-drop of an image file onto the canvas, and paste-image-from-clipboard.
  • loadImages() — returns the consumer's image list for the "Library" picker dialog (grid + filter by alt/URL).
  • deleteImage(url) — removes an image from the library; surfaces a delete button on hover in the picker.

Reference upload handler:

const uploadImage = async (file: File) => {
  const form = new FormData();
  form.append('file', file);
  const res = await fetch('/api/images', { method: 'POST', body: form });
  return res.json(); // { url, width, height }
};

Newly uploaded images get their width / height set on the resulting Image block — important for Outlook, which needs explicit dimensions to lay the email out before images load.

Example PostgreSQL schema

A minimal schema that maps directly onto SavePayload, TemplateListItem, and the image-library callbacks. The editor itself is backend-agnostic — this is just a reference starting point.

-- Email templates (user-editable rows + org-wide read-only samples)
create table email_templates (
  id            uuid primary key default gen_random_uuid(),
  slug          text not null,                                  -- primary label shown in the drawer
  kind          text not null check (kind in ('template', 'sample')),
  description   text,
  subject       text,                                           -- mirrors editor_config.root.data.subject
  variables     jsonb not null default '[]'::jsonb,             -- [{ name, description?, sampleValue? }]
  tags          text[] not null default '{}',
  thumbnail_url text,

  -- Source of truth: full TEditorConfiguration from SavePayload.editorConfig
  editor_config jsonb not null,

  -- Pre-rendered output from the save pipeline; usually what you ship to the sender
  body_html     text not null,
  body_text     text not null,

  -- null for org-wide samples, set for user/tenant-owned templates
  owner_id      uuid references users(id) on delete cascade,

  created_at    timestamptz not null default now(),
  updated_at    timestamptz not null default now(),

  unique (owner_id, slug)
);

create index email_templates_owner_kind_idx on email_templates (owner_id, kind);
create index email_templates_tags_gin_idx   on email_templates using gin (tags);

-- Image library backing uploadImage / loadImages / deleteImage
create table email_images (
  id            uuid primary key default gen_random_uuid(),
  url           text not null unique,
  thumbnail_url text,
  width         int,
  height        int,
  alt           text,
  owner_id      uuid references users(id) on delete cascade,
  uploaded_at   timestamptz not null default now()
);

create index email_images_owner_uploaded_idx on email_images (owner_id, uploaded_at desc);

loadTemplates typically scopes to owner_id = :user with kind = 'template'; loadSamples returns kind = 'sample' (often ignoring owner_id, or scoping to an org). The kind column — not which endpoint returned the row — decides which drawer section a template appears in.

| theme | object | theme.ts | Custom theme for the EmailEditor, must be a Material UI theme object |

Imperative API

You can access the EmailEditor's methods using a ref:

import { EmailEditor } from 'kontakto-email-editor';
import { useRef } from 'react';

function MyApp() {
  const editorRef = useRef(null);
  
  const handleSave = () => {
    const template = editorRef.current.saveTemplate();
    console.log('Saved template:', template);
  };
  
  const handleLoad = (template) => {
    editorRef.current.loadTemplate(template);
  };
  
  const handleGetCurrent = () => {
    const current = editorRef.current.getTemplate();
    console.log('Current template:', current);
  };
  
  return (
    <div>
      <button onClick={handleSave}>Save</button>
      <button onClick={handleGetCurrent}>Get Current</button>
      <EmailEditor ref={editorRef} minHeight="600px" />
    </div>
  );
}

Keyboard shortcuts

The editor surfaces a few global shortcuts. Inside text fields or rich-text blocks the native browser behavior takes over — global undo/redo only fires when focus is on the canvas (not mid-typing).

| Action | macOS | Windows / Linux | |---|---|---| | Undo | ⌘ + Z | Ctrl + Z | | Redo | ⌘ + Shift + Z | Ctrl + Shift + Z, Ctrl + Y | | Bold (inside text) | ⌘ + B | Ctrl + B | | Italic (inside text) | ⌘ + I | Ctrl + I | | Link (inside text) | ⌘ + K | Ctrl + K |

The undo history tracks document mutations only (block insert/delete/move, content edits, style changes, variable edits). Rapid consecutive edits (dragging a slider, typing a burst) collapse into a single history entry. The ring buffer holds the last 100 entries and resets whenever a new template is loaded.

Stand-alone version using Vite

This project includes a standalone version that can be run using Vite:

# Install dependencies
npm install

# Run the development server
npm run dev

This will start a development server with the EmailEditor running as a standalone application that uses browsers local storage to save and load templates.

Theming

The EmailEditor component has the CssBaseline and ThemeProvider components from Material UI applied by default. However, if you need to supply a custom theme, you can do so by passing a custom theme to the EmailEditor component. The theme should be a Material UI theme object.

Development

To run this locally:

  1. Clone the repository
  2. Install dependencies:
    npm install
  3. Start the development server:
    npm run dev
  4. Visit http://localhost:5173/ in your browser

Project Structure

  • src/blocks/ - Email components (text, images, buttons, etc.)
  • src/editor/ - Core editor functionality
  • src/app/ - Main application components
  • src/email-builder/ - Email template rendering
  • src/core/ - Core utilities and types

Technologies

  • React
  • TypeScript
  • Material UI
  • Zustand for state management
  • Vite as the build tool
  • Marked and Highlight.js for markdown and code highlighting

Email-Template-Editor

Licensed under the MIT License. See LICENSE for details.

This project, Email-Template-Editor, is a substantial derivative work based on an original MIT-licensed project, email-builder-js by Waypoint (Metaccountant, Inc.). email-builder-js is a free and open-source block-based email template builder designed for developers to create emails with JSON or HTML output. While the original code was created by Waypoint, this project has been significantly refactored with:

  1. Restructuring of the project files and directories.
  2. Implementation of how external context is handled.
  3. Changes to the purpose of the project to be integrated and embedded into other React based projects.

Original Code from Waypoint (Metaccountant, Inc.)

The following parts (not limited to) of this project are derived from the original MIT-licensed project email-builder-js by Waypoint:

  • The parsing logic is based on Waypoint's original block-based parsing approach
  • The concepts for the blocks
  • The concepts for the editor
  • The concepts of the builder

Acknowledgements

This project gratefully acknowledges the original work by Waypoint (Metaccountant, Inc.) on email-builder-js as the foundation upon which this version was built.