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-bridge

v0.2.0

Published

Write transactional emails in React Email, export to HTML with template markers (Handlebars, Liquid, Mailchimp). Live preview, real engine, copy-paste into VTEX, Mandrill, etc.

Readme

react-email-bridge

Write transactional emails in React Email, export to HTML + template markers (Handlebars). Live preview against real data — paste straight into VTEX, Mandrill, Mailchimp, or any platform that interprets {{variable}} syntax.

npm license: MIT tests: 111


Why this exists

VTEX Message Center, Mandrill, Mailchimp, and most transactional-email platforms render templates server-side via Handlebars (or a Handlebars-compatible engine). You paste raw HTML with {{customer.firstName}}, {{#each items}}…{{/each}} markers, and the platform fills them in per send.

The pain: writing and maintaining that HTML by hand is brutal.

  • Tables for layout (Outlook compat).
  • CSS inlining (Gmail strips <style> tags).
  • Helper soup ({{formatCurrency value}}, {{#compare a '==' b}}, {{#group items by="addressId"}}).
  • No componentization. No type safety. No live preview against your real data shape.

React Email solves authoring brilliantly — you write components, get auto inlined CSS, get table layout for free. But it has no story for template markers: its render either produces final HTML or breaks on {{ characters.

react-email-bridge connects the two:

<Heading>Olá {`{{customer.firstName}}`}!</Heading>

<Each path="items">
  <Row>
    <Text>{`{{name}}`} — R$ {`{{formatCurrency price}}`}</Text>
  </Row>
</Each>

…compiles to clean Handlebars-ready HTML, previews live with the real engine running against a .json fixture, and exports a .hbs file you paste directly into VTEX (or wherever).


What you get

| | | |---|---| | ✓ Authoring | Write .tsx with React Email components | | ✓ Markers | Variables, loops, conditionals, sub-expressions, custom helpers — all preserved | | ✓ Live preview | Real Handlebars + real fixture data, hot reloaded on save | | ✓ Editor UI | Sidebar, code view (React / HTML / Plain Text / Data), Compatibility/Linter/Spam tabs, dark mode, responsive toggle, Resend send | | ✓ Export | pnpm export.hbs file, CSS inlined, ready to paste | | ✓ Helper coverage | handlebars-helpers (~150 helpers) + VTEX-flavored fakes (formatCurrency, formatDate, math, group, richShippingData, …) | | ✓ Fixture story | <basename>.json adjacent to the template; banner + one-click creation if missing |


Install

npm install react-email-bridge
# or
pnpm add react-email-bridge

The CLI is bundled. The live preview server (react-email-bridge-ui) is fetched on-demand the first time you run dev — you don't need to install it explicitly.


Quickstart — minimal project in <60 seconds

npx react-email-bridge init my-emails
cd my-emails
npm run dev          # http://localhost:3737

init scaffolds a complete project, runs <your-package-manager> install, and initializes a git repo. The package manager is auto-detected from how you invoke the CLI (npm, pnpm, yarn, or bun).

my-emails/
├── emails/
│   ├── welcome.tsx          # your template
│   └── welcome.json         # fixture used for preview only
├── .npmrc
├── react-email-bridge.config.ts
├── tsconfig.json
└── package.json

Open the URL the dev server prints. You'll see:

  • Left sidebar — list of your .tsx templates (auto-discovered from emails/).
  • Center iframe — live preview with Handlebars + fixture data interpolated.
  • Bottom tabs — Compatibility check, Linter, Spam score, Resend send.
  • Top toggle — switch to code view (React / HTML / Plain Text / Data).
  • Right edge — drag to resize for desktop/mobile.

Edit emails/welcome.tsx or emails/welcome.json — the iframe reloads automatically.

Want a richer starting point?

Pass --template vtex-store for 13 pre-ported VTEX transactional templates + shared partials + Tailwind config:

npx react-email-bridge init my-vtex-emails --template vtex-store

Same flow — no need to clone the repo. Templates are fetched on-demand from GitHub, pinned to your installed CLI version.

Options

npx react-email-bridge init <dir> [options]

  -t, --template <name>   Template to use (default: generic-hbs)
  --skip-install          Skip running <pm> install
  --skip-git              Skip git init + initial commit

Export

npm run export

Writes dist/welcome.hbs:

<!DOCTYPE html ...>
…
<h1 style="color:{{theme.primaryColor}}">Welcome, {{customer.firstName}}!</h1>
<p>Your order <strong>#{{orderId}}</strong> is confirmed.</p>
{{#each items}}
  <p>• {{name}} × {{quantity}}</p>
{{/each}}
{{#if trackingUrl}}
  <p><a href="{{trackingUrl}}">Track your order</a></p>
{{else}}
  <p>Tracking will be available soon.</p>
{{/if}}
…

Copy-paste into VTEX Message Center's HTML field. VTEX fills the markers per send. Done.


Writing a template

import {
  Html, Head, Body, Container, Heading, Text, Link, Section, Row, Column, Img,
} from '@react-email/components';
import { hbs } from 'react-email-bridge';
import { Each, If, Unless, Else, Raw } from 'react-email-bridge/hbs';

export default function OrderConfirmed() {
  return (
    <Html>
      <Head />
      <Body style={{ backgroundColor: '#fff' }}>
        <Container>
          {/* Variable in a string attribute — write the marker directly */}
          <Img src={`{{logoUrl}}`} alt={`{{accountName}}`} width="180" />

          {/* Variable in style — use hbs() because React rejects non-string style values */}
          <Heading style={{ color: hbs('theme.primaryColor') }}>
            Olá {`{{customer.firstName}}`}!
          </Heading>

          {/* Loop */}
          <Each path="items">
            <Row>
              <Column><Text>{`{{name}}`}</Text></Column>
              <Column><Text>R$ {`{{formatCurrency price}}`}</Text></Column>
            </Row>
          </Each>

          {/* Conditionals */}
          <If path="hasInvoice">
            <Text><Link href={`{{invoiceUrl}}`}>Ver nota fiscal</Link></Text>
            <Else />
            <Text>Nota fiscal em breve.</Text>
          </If>

          {/* Comparator with operator */}
          <If compare={['items.length', '>', '1']}>
            <Text>Você comprou múltiplos itens.</Text>
          </If>

          {/* Negation */}
          <Unless path="cancelled">
            <Text>Pedido ativo.</Text>
          </Unless>

          {/* Escape hatch: any arbitrary HBS via <Raw> */}
          <Raw>{`{{#group items by="category"}}`}</Raw>
            <Text>Categoria {`{{value}}`}:</Text>
            <Each path="items">
              <Text>- {`{{name}}`}</Text>
            </Each>
          <Raw>{`{{/group}}`}</Raw>
        </Container>
      </Body>
    </Html>
  );
}

And the fixture OrderConfirmed.json for preview:

{
  "theme": { "primaryColor": "#0066ff" },
  "customer": { "firstName": "Marco" },
  "logoUrl": "https://…/logo.png",
  "accountName": "My Store",
  "items": [
    { "name": "Helmet", "price": 19990, "category": "safety" },
    { "name": "Gloves", "price": 8990, "category": "safety" }
  ],
  "hasInvoice": true,
  "invoiceUrl": "https://…/inv/123",
  "cancelled": false
}

API

react-email-bridge

import { render, hbs, defineConfig, type Preset, type BridgeConfig } from 'react-email-bridge';

| Export | Purpose | |---|---| | render(element, { inlineCss? }) | Renders a React element to HTML with markers preserved. CSS inlined by default. | | hbs(path: string) | Sentinel for use inside style objects. Becomes {{path}} in output. | | defineConfig({ … }) | Typed helper for react-email-bridge.config.ts. | | Preset, BridgeConfig | Public types. |

react-email-bridge/hbs

import { Each, If, Unless, Else, Raw, previewWithFixture } from 'react-email-bridge/hbs';

| Component | Emits | |---|---| | <Each path="items"> | {{#each items}}…{{/each}} | | <Each path="items" as="item"> | {{#each items as \|item\|}}…{{/each}} | | <If path="x"> | {{#if x}}…{{/if}} | | <If eq={['a', '"b"']}> | {{#eq a "b"}}…{{/eq}} | | <If compare={['a', '==', 'b']}> | {{#compare a '==' b}}…{{/compare}} | | <Unless path="x"> | {{#unless x}}…{{/unless}} | | <Else /> | {{else}} (valid in #if, #unless, #each) | | <Raw>{\…`}` | Children emitted as text — escape hatch for any custom HBS |

Config file

react-email-bridge.config.ts is optional and loaded from your project root:

import { defineConfig } from 'react-email-bridge';

export default defineConfig({
  outputExtension: '.html',           // default '.hbs'
  strict: true,                       // default false — throw on missing vars
  previewHelpers: {
    uppercase: (s) => String(s).toUpperCase(),
  },
});

CLI

| Command | What it does | |---|---| | react-email-bridge init [dir] | Scaffold emails/ with one example template + fixture + config | | react-email-bridge dev | Start the live preview server (default port 3737) | | react-email-bridge export <name> | Build one template to dist/<name>.hbs | | react-email-bridge export --all | Build every template |


Migration from vtex-emails Handlebars

If you have an existing Handlebars-based VTEX email project (e.g. forked from vtex-email-framework), porting is mechanical:

| Old (.hbs) | New (.tsx) | |---|---| | Raw HTML tables | React Email components (<Container>, <Section>, <Row>, <Column>) | | {{firstName}} in text | {\{{firstName}}`}(template literal in JSX) | |{{#each items}}…{{/each}}|| |{{#compare a '==' b}}…{{/compare}}|<If compare={['a', '==', 'b']}>…| |{{#unless cancelled}}…{{/unless}}|| |{{#each items}}…{{else}}…{{/each}}|<Each…>| |{{#group items by="x"}}…{{/group}}|{`…`} | | Custom helpers ({{formatCurrency 100}}) | Same string in JSX — preview runtime ships fakes for the common VTEX set | | Partials ({{> logo}}) | React components () — React flattens them at render | | data/*.jsonfixture per template |emails/.json(1:1 same shape) | |yarn distto produce final HTML |react-email-bridge export --all` |

See examples/vtex-store/ in this repo for a real-world VTEX template port using the densest patterns: #each orders, #richShippingData, #group by addressId, #group by packageId, #math index '+' 1, (math @index "%" 2) sub-expressions.


Examples in this repo

| Folder | Demonstrates | |---|---| | examples/generic-hbs/ | Agnostic baseline — all sugar components, helpers, sub-expressions. Also the source for the default npx react-email-bridge init scaffold. | | examples/vtex-store/ | 13 real VTEX transactional templates, Tailwind-styled with the Halo design system, shared partials. Also the source for new-project --template vtex-store. |

Examples are workspace-bound (workspace:* dep on the core) and run as part of CI smoke tests. The same directory tree is what gets copied into a user project when scaffolding — see ADR-0001.


Architecture

react-email-bridge is a monorepo with two npm packages:

react-email-bridge/                  # this repo
├── packages/
│   ├── react-email-bridge/          # CLI + core + HBS preset  (~38KB built)
│   └── react-email-bridge-ui/       # Next.js preview server (forked @react-email/ui, MIT)
├── examples/
│   ├── generic-hbs/                 # minimal — also the default scaffold
│   └── vtex-store/                  # 13 VTEX templates — also the vtex-store scaffold
├── scripts/new-project.ts           # author shortcut — thin shim over the published `init`
├── validation/                      # P0 end-to-end stress test (28 asserts)
└── docs/
    ├── adr/                         # architectural decisions
    └── internal/                    # contributor-only docs (DECISIONS, CONTEXT, PATCHES, …)

Pipeline:

TSX  →  @react-email/render  →  substitute hbs() sentinels  →
unescape markers (Plano B)   →  juice CSS inline  →  unescape again
                                            │
                          ┌─────────────────┴──────────────────┐
                          │                                    │
                  Handlebars.compile(html)(fixture)        Write to .hbs
                          │
                       iframe in editor

Read docs/internal/DECISIONS.md for the 16 frozen v0.1 design choices (sentinel format, strict mode default, helper registration strategy, …) and docs/adr/ for decisions from v0.2 onwards.


Tests

pnpm test

111 vitest tests across 8 files covering:

  • Sugar components (Each, If, Unless, Else, Raw) — output strings.
  • Sentinel substitution + marker unescape (Plano B for React entity escaping).
  • VTEX-flavored fake helpers (formatCurrency, formatDate, math, group, eval, richShippingData, etc) — input/output pairs.
  • helperMissing semantics (variable lookup vs unknown helper).
  • Preview runtime with config-supplied helper overrides.
  • Full render() pipeline against every marker context (text, attr, style, sub-expressions, nested blocks).
  • CLI commands (init, export, export --all, error paths, config-driven extension).
  • End-to-end fixture interpolation (Handlebars + .json).

Plus pnpm validate runs a stress-test template through the export pipeline with 28 assertions covering every marker context.


Contributing

See CONTRIBUTING.md for local setup, the git-hook gates, the changesets release flow, and the scaffolding workflow for working on the library itself.


Acknowledgments

  • @react-email/ui — the preview editor (sidebar, code view, compatibility/spam tabs, dark mode, resizable iframe) is a fork of @react-email/ui (MIT). All the heavy UI lift is theirs; we patched the rendering pipeline to add Handlebars interpolation against .json fixtures.
  • handlebars-helpers — covers ~150 of the helpers VTEX templates use in the wild.
  • vtex/vtex-emails — the reference official framework whose template patterns we cover end-to-end.

License

MIT. See LICENSE.

The vendored @react-email/ui retains its original MIT copyright (Bu Kinoshita and Zeno Rocha) — preserved in LICENSE.