Developer Tools

@unitpost/email

@unitpost/email builds HTML emails from React or a TSX string. Maintained by Unitpost, a sending service. Version 0.6.0, 4 stars (read 2026-11-09).

Olivier Bazoud
November 5, 2026
9 min read

@unitpost/email is a library for writing an email in TypeScript or JavaScript and rendering it to HTML and plain text. You either build a React component from its own components (Section, Heading, Text, Button and so on) and call render(), or write the same tags as a string and pass it to parseTsx().

At a glance
Language
TypeScript, JavaScript (React optional)
License
MIT, free
Maintainer
Unitpost
Latest release
0.6.0 (2026-11-08)
Weekly npm downloads
414

Figures read on November 9, 2026.

Good fit
  • A TypeScript team that keeps emails in its repository
  • Teams that prefer a template held in a string, with no build step
  • Any sending provider: rendering needs no account or API key
  • A Node.js runtime: 18 or newer is declared, CI runs 20
Not a fit
  • A backend outside JavaScript
  • A project that needs a 1.0 release or frozen HTML output
  • A project that cannot take zod as a peer dependency

Minimal example

This is the README's React example (quickstart steps 2 and 3).

TypeScript
// emails/welcome.tsx
import { Section, Heading, Text, Button, render } from '@unitpost/email/react';
export function WelcomeEmail({ name }: { name: string }) {
return (
<Section>
<Heading>Welcome, {name}</Heading>
<Text>Thanks for signing up. Confirm your email to get started.</Text>
<Button href="https://example.com/confirm">Confirm email</Button>
</Section>
);
}
// render
const html = render(<WelcomeEmail name="Ada" />);

render() returns an HTML string synchronously and does not use React DOM. Here is the same email without React, from the README's "Without React" section.

TypeScript
import { parseTsx, renderToHtml } from '@unitpost/email';
const doc = parseTsx(`
<Section>
<Heading>Welcome, {{first_name}}</Heading>
<Button href="https://example.com/confirm">Confirm email</Button>
</Section>
`);
const html = renderToHtml(doc, { first_name: 'Ada' });

{{first_name}} is a variable filled by the second argument of renderToHtml() (or of render()), and the values are HTML-escaped.

Pros and cons

Pros
  • MIT license, no account to render
  • React and TSX-string authoring paths
  • Plain-text part from renderToText()
  • Local preview server with plain-text and dark views
  • One runtime dependency, no CSS inliner
Cons
  • Pre-1.0, rendered HTML can change
  • Unknown Tailwind classes dropped silently
  • Component props override className
  • Hooks and plain HTML throw in React
  • Small adoption

The details behind each point are in How it works, Limits and gotchas and Maintenance.

How it works

The renderer emits table layout: the source writes <table role="presentation" border="0" cellpadding="0" cellspacing="0"> elements, and the project describes the output as table layout with inline CSS.

  • Inlining: className (Tailwind-style) and customCss compile to inline style attributes at render time, in a fixed order: theme defaults, className, schema props, then customCss. There is no separate inliner dependency, and the one runtime dependency is sanitize-html. sm: and hover: variants are hoisted into a <style> block in the <head>. That block always holds the fluid-body rule, the column-stacking rules at 600px, those hoisted rules, then either the dark-mode CSS or a light-only color-scheme declaration.
  • Outlook: the 0.4.0 changelog lists VML and Office namespaces on <html>, OfficeDocumentSettings at 96 DPI, mso-table-lspace and mso-table-rspace set to 0, and mso-line-height-rule: exactly. It adds a fixed-width ghost table holding the card at theme.contentWidth, because Outlook for Windows ignores max-width. The changelog says this applies to "every sent Design email". We rendered the published 0.6.0 package and read the HTML: the React render() output carries the same MSO conditionals and namespaces, and we did not open it in a mail client. The buttons are padded tables with no VML. Since 0.6.0, a Section with a background image gets a VML rectangle.
  • Plain text: renderToText(doc) returns a plain-text string to send next to the HTML. React elements go through fromJsx() first.
  • Preview: npx @unitpost/email dev lists the emails in emails/, reloads on save and shows each at desktop and mobile width, light and dark, with HTML, plain-text and design lint views. It listens on 127.0.0.1 only and uses esbuild to bundle React emails.

The HTML and text parts you send travel as a MIME message. MIME covers how the parts are packaged and message format covers the headers around them.

Send it with an email API

Render the email, then pass the HTML and the plain-text part to the Nuntly SDK.

TypeScript
import { Nuntly } from '@nuntly/sdk';
import { parseTsx, renderToHtml, renderToText } from '@unitpost/email';
const nuntly = new Nuntly({ apiKey: process.env.NUNTLY_API_KEY! });
const doc = parseTsx(`
<Section>
<Heading>Welcome, {{first_name}}</Heading>
</Section>
`);
const vars = { first_name: 'Ada' };
const html = renderToHtml(doc, vars);
const text = renderToText(doc, vars);
await nuntly.emails.send({
from: 'Acme <hello@example.com>',
to: 'jane@example.org',
subject: 'Welcome',
html,
text,
});

The Send Your First Email guide covers the SDK setup.

Email client support

Claimed by the project
  • Outlook
  • Gmail
  • Apple Mail
  • Outlook for Windows layout fixes
  • Opt-in dark mode
Not published
  • Client versions tested
  • Screenshots or test results
  • Test method

The README opens with "Email component library that works in Outlook, Gmail, and Apple Mail", and the 0.4.0 changelog describes the Outlook settings. We did not test the output in a mail client.

Outlook on Windows is the risky one. The renderer adds a fixed-width ghost table, but the Button puts its padding on the link and its corners on border-radius, and caniemail lists neither as supported there, so expect a square, tight button (inferred from the markup). Set width on every Image. Dark mode is off by default, and caniemail lists prefers-color-scheme as unsupported in every Gmail client and in Outlook on Windows, where Gmail applies its own inversion. The renderer loads no web fonts, and caniemail lists base64 images as unsupported in Gmail. Before you ship, send a test to Outlook on Windows and Gmail.

Limits and gotchas

  • Component props override className. Props with a default (Text fontSize and marginBottom, Section padding, Heading size, Button color) win over Tailwind-style classes, so text-lg, p-4 or bg-red-600 on those components change nothing, even in the vendor's own samples. Set the prop, or use customCss, which is applied last.
  • Unsupported classes are dropped without a warning. className is a curated subset of Tailwind: only part of the color palette, and md:, lg: and dark: classes are discarded, with no message from the preview lint. sm: means 600px and below, not a min-width breakpoint, and its rule has no !important, so it cannot override a value the element already sets inline (padding, font size). Check compileClasses(className).unknown when a style does not apply.
  • Outlook for Windows is untested, and two components are at risk. Button puts its padding on the link and its corners on border-radius, and caniemail says Outlook for Windows supports neither there, so the button probably renders square and tight (inferred from the markup, not seen in Outlook). Set width on every Image, because without it there is no width attribute and Outlook ignores CSS width on images.
  • React components are not rendered by React. Your function components are called as plain functions and lowered to the email document, so hooks, Context providers, plain HTML elements such as <div> and async components throw. Pass data in as props and return only catalog components.
  • Missing variables ship as written. A {{token}} without a value stays in the HTML, so check resolveVariables(doc, values).missing before you send. With category="marketing" the renderer adds a fixed English unsubscribe footer whose link is {{unsubscribe_url}}, so pass that value yourself.

Maintenance

Active but pre-1.0: eleven releases in four months, the latest on 2026-11-08, the same day as the last commit.

  • Pin the version. The changelog counts a change to the rendered HTML as breaking, and 0.5.0 shipped one: the "Sent with Unitpost" mark became opt-in.
  • Expect a mirror. The GitHub repository only receives commits synced from another Unitpost repository, and pull requests are imported, not merged.
  • Take versions from npm. The repository has no GitHub Releases and no git tags.

Compare

  • React Email: React components, maintained by another sending service.
  • JSX Email: React components too, with an individual maintainer.
  • MJML: XML-style markup instead of JSX.
  • mjml-react: React components that compile through MJML.
  • Maizzle: Vue single-file components with Tailwind CSS.
  • better-svelte-email: Svelte 5 components.
  • Hermes: a Go library that fills a struct instead of a template.
  • The email frameworks hub: every tool, with its status and read date.

FAQ

What is @unitpost/email?

It is an open-source (MIT) TypeScript library from Unitpost that turns React components, or the same tags written as a TSX string, into HTML and plain-text emails. The npm package is @unitpost/email and the latest version is 0.6.0, read 2026-11-09.

Do I need a Unitpost account to use it?

No, according to the README: rendering needs no account or API key, and the HTML works with any provider. The README's first send example and the preview server's "Send test" button do use Unitpost, so you wire up your own sender for production.

Can I use it without React?

Yes. Write the tags as a string, parse it with parseTsx(source) and render the document with renderToHtml(doc). The README says this needs no build step, and a golden test checks that the React and string paths render the same email.

Is @unitpost/email production ready?

It is pre-1.0 (0.6.0), published since 2026-07-12, with 4 GitHub stars and 414 weekly npm downloads (read 2026-11-09). The changelog treats a change to rendered HTML as breaking and 0.5.0 shipped one, so pin the version and compare the rendered output when you upgrade.

Does it handle Outlook?

The project says it works in Outlook, Gmail and Apple Mail, and its 0.4.0 changelog describes MSO settings and a fixed-width ghost table for Outlook for Windows. It publishes no per-client test results, and the Button is expected to render square in Outlook on Windows, so test your own templates.