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

react-email-rte

v0.1.0

Published

A React rich-text editor built on TipTap v3, purpose-built for composing HTML emails. Preserves inline styles, the full document shell, and produces email-safe (bulletproof) markup.

Readme

react-email-rte

A React rich-text editor built on TipTap v3, purpose-built for composing HTML emails.

Editing email HTML is not like editing article HTML. This package is designed around three constraints that ordinary rich-text editors get wrong:

  1. Emails are full documents<!doctype html><html><body style="…"><table>…. A contentEditable/ProseMirror surface cannot host <html>/<head>/<body>; it silently strips them. So this editor edits only the body fragment and preserves the surrounding document shell verbatim (the document-shell bridge).
  2. Emails rely on inline stylesstyle="padding:…;background:…" on buttons, colored headings, table layouts. TipTap strips the style attribute by default. The bundled PreserveStyles extension keeps it.
  3. Email clients need bulletproof markup — CTA buttons are <a style="display:inline-block;…"> wrapped in an aligned <p>, not <button>.
  • ✅ Ships ESM + CJS + type declarations
  • ✅ Works in Next.js App Router and plain Vite/CRA (immediatelyRender:false set internally for SSR)
  • ✅ No Tailwind/shadcn requirement — plain CSS you import
  • ✅ TypeScript strict, no any in public types

Install

npm i react-email-rte
# peer deps (you already have these in a React app):
npm i react react-dom
import { EmailEditor } from "react-email-rte";
import "react-email-rte/styles.css"; // required

Usage

<EmailEditor> — batteries included (default export)

Handles the document-shell bridge, placeholder chips, and image upload wiring. value may be a full <!doctype><html><body>… document or a bare fragment; onChange always emits in the same shape it received.

import { useState } from "react";
import { EmailEditor } from "react-email-rte";
import "react-email-rte/styles.css";

export default function Composer() {
  const [html, setHtml] = useState(
    "<html><body><h1>Hi {{FirstName}}</h1></body></html>",
  );

  return (
    <EmailEditor
      value={html}
      onChange={setHtml}
      placeholders={[
        { token: "{{FirstName}}", label: "First name" },
        { token: "{{Email}}", label: "Email" },
      ]}
      onUploadImage={async (file) => {
        // upload `file` somewhere and return a hosted URL
        return "https://cdn.example.com/x.png";
      }}
      minHeight={288}
    />
  );
}

<RichTextEditor> — the generic body-HTML editor (named export)

The toolbar + editable surface operating on a plain HTML fragment (no <html>/<body>). EmailEditor is a thin wrapper that adds the shell bridge + chips around this. Use it if you want to build your own wrapper.

import { useRef, useState } from "react";
import { RichTextEditor, type RichTextEditorHandle } from "react-email-rte";
import "react-email-rte/styles.css";

function Body() {
  const [body, setBody] = useState("<p>Body HTML only.</p>");
  const ref = useRef<RichTextEditorHandle>(null);

  return (
    <>
      <button onClick={() => ref.current?.insertAtCaret("{{FirstName}}")}>
        Insert token
      </button>
      <RichTextEditor
        ref={ref}
        value={body}
        onChange={setBody}
        editorStyle={{ minHeight: 240 }}
        toolbar={{ button: false }} // hide the CTA-button control
      />
    </>
  );
}

Props

EmailEditorProps

| Prop | Type | Default | Description | | --------------- | ----------------------------------------- | -------------- | ----------- | | value | string | — | Full HTML document or bare body fragment. | | onChange | (value: string) => void | — | Fires with HTML in the same shape as value (shell re-applied). | | placeholders | EmailPlaceholder[] | undefined | Merge tokens → renders a chip row that inserts at the caret. | | onUploadImage | (file: File) => Promise<string> | undefined | Resolve an upload to a URL. If omitted, the image button prompts for a URL. | | minHeight | number | 288 | Min height (px) of the editable surface. | | placeholder | string | "Write your email…" | Empty-state text. | | toolbar | ToolbarConfig | all on | Which controls to render. | | className | string | undefined | Extra class on the wrapper. |

RichTextEditorProps

| Prop | Type | Default | Description | | --------------- | ----------------------------------- | ------- | ----------- | | value | string | — | Body-level HTML fragment. | | onChange | (value: string) => void | — | Fires with the body-level HTML fragment. | | placeholder | string | "Write your email…" | Empty-state text. | | onUploadImage | (file: File) => Promise<string> | undefined | Upload handler (else URL prompt). | | className | string | undefined | Extra class on the wrapper. | | editorStyle | React.CSSProperties | undefined | Applied to the editable surface (e.g. { minHeight }). | | toolbar | ToolbarConfig | all on | Which controls to render. |

RichTextEditor is a forwardRef exposing RichTextEditorHandle:

type RichTextEditorHandle = { insertAtCaret: (htmlOrText: string) => void };

ToolbarConfig

Every key defaults to true. Set one to false to hide that control/group.

type ToolbarConfig = {
  inline?: boolean;   // Bold / Italic / Underline / Strikethrough / Code
  headings?: boolean; // H2–H6
  lists?: boolean;    // Bullet / Ordered / Quote
  align?: boolean;    // Left / Center / Right
  link?: boolean;
  image?: boolean;
  button?: boolean;   // Email CTA button dialog
  blocks?: boolean;   // Paragraph / Divider / Footer presets
  html?: boolean;     // Raw HTML source toggle
};

Building blocks (also exported)

For advanced use, the underlying pieces are exported:

import {
  PreserveStyles,        // the TipTap extension that keeps inline `style`
  createEmailExtensions, // the full extension set as a factory
  buildButtonHtml,       // (config: EmailButtonConfig) => string
  EmailButtonDialog,     // the button-config modal
  splitEmailHtml,        // (html) => { prefix, body, suffix }
  joinEmailHtml,         // (shell, body) => html
  extractEmailBody,      // (html) => body fragment
  escapeHtml,
  presets,               // { paragraph, divider, footer, heading }
} from "react-email-rte";

import type {
  EmailButtonConfig,
  EmailPlaceholder,
  ToolbarConfig,
  EmailHtmlDocument,
} from "react-email-rte";

buildButtonHtml

const html = buildButtonHtml({
  text: "Get started",
  href: "https://example.com",
  bgColor: "#2563eb",
  textColor: "#ffffff",
  align: "center",
  radius: 6,
  fullWidth: false,
});
// → <p style="margin:16px 0;text-align:center"><a href="…" style="display:inline-block;…">Get started</a></p>

The anchor is wrapped in a <p> (not a <div>) on purpose: TipTap has no div node and would unwrap a <div>, losing the alignment. A paragraph's text-align is preserved by the TextAlign extension.

Theming

styles.css targets .rte-* classes and the .ProseMirror/.rte-content surface, and exposes CSS custom properties with neutral defaults. Override any of them on a parent element (or :root):

.my-editor {
  --rte-accent: #7c3aed;
  --rte-accent-contrast: #ffffff;
  --rte-border: #e2e8f0;
  --rte-border-strong: #cbd5e1;
  --rte-bg: #ffffff;
  --rte-bg-muted: #f8fafc;
  --rte-fg: #0f172a;
  --rte-fg-muted: #64748b;
  --rte-radius: 10px;
  --rte-active-bg: #f5f3ff;
}
<EmailEditor className="my-editor" value={html} onChange={setHtml} />

SSR / Next.js App Router

  • The component entry is marked "use client", so importing it from a Server Component is fine — render <EmailEditor> inside a client boundary.
  • immediatelyRender: false is set internally, which prevents the hydration mismatch TipTap otherwise throws under SSR. You don't need to configure anything.

Built-in extensions

The editor is configured with TipTap v3:

  • StarterKit (which already bundles Link, Underline, lists, blockquote, code, and headings — do not add @tiptap/extension-link or @tiptap/extension-underline yourself), with Link configured openOnClick:false, autolink:true, rel="noopener noreferrer".
  • @tiptap/extension-image (inline:false, allowBase64:false)
  • @tiptap/extension-text-align (["heading","paragraph"])
  • @tiptap/extension-text-style (TextStyle + Color, re-exported here — no separate @tiptap/extension-color needed)
  • @tiptap/extension-highlight (multicolor:true)
  • @tiptap/extension-placeholder (StarterKit v3 does not bundle it; added explicitly for the empty-state hint, which sets the is-editor-empty/is-empty class and data-placeholder attribute that styles.css renders)
  • PreserveStyles (this package)

License

MIT