@unitpost/email

Component library

Build emails from a small set of cross-client-tested components. Author them visually in the template editor, or in code using the constrained TSX dialect below — the same components render identically in the editor preview and in the email your recipients receive.

Introduction

A small set of cross-client-tested components for building email.

The Unitpost component library is a constrained set of building blocks for email — layout, typography, media, and interactive elements — designed so the same document renders pixel-for-pixel in every inbox, Outlook included. Under the hood each component compiles to the table-based, inline-styled HTML that email clients actually respect, so you never touch archaic markup yourself.

Everything on this page is powered by the `@unitpost/email` package — the exact renderer the send engine uses. Author your email visually in the template editor, or in code using the constrained TSX dialect below; both produce the same document, and the preview you see here is the email your recipients get.

Not React Email

The dialect looks like React, but it's a small, fixed vocabulary (the components below) rather than arbitrary JSX. That constraint is what lets the visual editor and the code editor stay perfectly in sync and guarantees cross-client output.

Insert dynamic data anywhere with `{{variable}}` — it's substituted at send time from the values you pass to the API, a contact's fields, or a campaign default.

Installation

Add the package and render your first email in a few minutes.

You don't need to install anything to use the components in the in-app editor — open any template, switch to Code mode, and start composing. To render or send emails from your own codebase, add the package below.

1. Install the package

Install `@unitpost/email` (plus its `zod` peer dependency) — it ships the component types, the parser, and the same `renderToHtml` renderer the send engine uses.

Bash
# npm
npm install @unitpost/email zod

# pnpm
pnpm add @unitpost/email zod

# yarn
yarn add @unitpost/email zod

# Requires Node.js 18+.

2. Write an email template

Compose a document from the components. In the editor's Code mode you write just the body markup; in your own code you build the same document and hand it to the renderer.

Node.js
import { parseTsx, renderToHtml, resolveVariables } from "@unitpost/email";

// The same constrained TSX you'd write in the editor's Code mode.
const doc = parseTsx(`
  <Section padding-y={32}>
    <Heading level={1}>Welcome, {{first_name}}</Heading>
    <Text>Thanks for joining. Confirm your email to get started.</Text>
    <Button href="https://example.com/verify?token={{token}}">
      Verify email
    </Button>
  </Section>
`);

// Fill {{variables}} and render to email-safe HTML.
const { values } = resolveVariables(doc, {
  first_name: "Ada",
  token: "abc123",
});
const html = renderToHtml(doc, values);

Prefer the visual builder?

You never have to write code — the template editor builds the identical document with drag-and-drop blocks, and you can flip to Code mode any time to see (or edit) the TSX.

3. Send it

Save the template in your workspace and reference it when you send, or pass the rendered HTML directly. Both go through the same delivery pipeline — see the API reference for the full send surface.

cURL
curl https://www.unitpost.com/api/v1/emails \
  -H "Authorization: Bearer $UNITPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "you@yourdomain.com",
    "to": "ada@example.com",
    "subject": "Welcome, {{first_name}}",
    "template_id": "tmpl_...",
    "variables": { "first_name": "Ada", "token": "abc123" }
  }'

4. Next steps

Explore the components below — start with Section and Row for layout, then Heading, Text, and Button for content. Every block also accepts the common props.

Components

Every building block, with a live preview and its props.

Layout

<Section>

Container

Use a Section to band content together — a hero, a card, a footer. It can hold any leaf block and can be nested inside another Section.

PropTypeDefaultDescription
background-colorcolorFill color behind the section.
padding-xnumber24Horizontal inner padding (px).
padding-ynumber24Vertical inner padding (px).
border-widthnumberBorder stroke width (px). 0/absent = no visible border.
border-styleenumsolid | dashed | dottedBorder stroke style.
border-colorcolorBorder stroke color.
border-radiusnumberCorner rounding (px). Independent of width — round a filled card without a stroke.

<Row>

Container

Rows render as a single table row with one cell per column, so they stay side-by-side even in Outlook. A Row may only contain Column components.

PropTypeDefaultDescription
column-gapnumber8Horizontal gap (px) between columns. Set 0 for flush.
stack-on-mobilebooleantrueBest-effort: let columns wrap on narrow viewports (table emails stay side-by-side in Outlook).
background-colorcolorFill color behind the row.
padding-xnumber24Horizontal inner padding (px).
padding-ynumber8Vertical inner padding (px).
border-widthnumberBorder stroke width (px). 0/absent = no visible border.
border-styleenumsolid | dashed | dottedBorder stroke style.
border-colorcolorBorder stroke color.
border-radiusnumberCorner rounding (px). Independent of width — round a filled card without a stroke.

<Column>

Container

Columns are table cells. `width` is a percentage of the row; the widths of the columns in a row should add up to ~100.

PropTypeDefaultDescription
widthnumber50Width as a percentage (1–100) of the parent Row.
background-colorcolorFill color behind the column.
padding-xnumber8Horizontal inner padding (px).
padding-ynumber0Vertical inner padding (px).
border-widthnumberBorder stroke width (px). 0/absent = no visible border.
border-styleenumsolid | dashed | dottedBorder stroke style.
border-colorcolorBorder stroke color.
border-radiusnumberCorner rounding (px). Independent of width — round a filled card without a stroke.

Content

<Heading>

A title (H1–H4). Inner text supports {{variables}}.

PropTypeDefaultDescription
levelenum1 | 2 | 3 | 42Heading level — controls size/weight.
alignenumleft | center | rightleftText alignment.
colorcolorText color.
font-sizenumberFont size (px). Overrides the level's default size (28/22/18/16 for H1–H4).
font-familystringOverride the document font for this heading.
font-weightstringFont weight (e.g. 400, 600, 700). Overrides the default bold heading weight.
line-heightstringLine height — a unitless multiplier (e.g. 1.3) or CSS length.
letter-spacingnumberLetter spacing (tracking) in px. Great for eyebrow labels.

<Text>

Inner content may include inline HTML for formatting: <strong>, <em>, <u>, <a href>, and <span style> (color / background-color). These round-trip through the visual editor.

PropTypeDefaultDescription
alignenumleft | center | rightleftText alignment.
colorcolorText color.
font-sizenumber16Font size (px).
font-familystringOverride the document font for this block.
font-weightstringFont weight (e.g. 400, 500, 600, 700).
line-heightstringLine height — a unitless multiplier (e.g. 1.5) or CSS length.
letter-spacingnumberLetter spacing (tracking) in px.

<Divider>

A thin horizontal rule to separate sections.

PropTypeDefaultDescription
colorcolor#e4e4e7Line color.

<Spacer>

Margins are unreliable across clients, so a Spacer renders as an explicit fixed-height cell.

PropTypeDefaultDescription
heightrequirednumber24Gap height (px).

<Markdown>

Author rich copy in Markdown — compiled to email-safe HTML.

PropTypeDefaultDescription
alignenumleft | center | rightleftAlignment.
colorcolorBase text color.
font-sizenumber16Base font size (px).

<Code>

A monospace code block — handy for API keys and snippets.

PropTypeDefaultDescription
background-colorcolor#f4f4f5Block background.
colorcolor#1a1a1aCode text color.

Media

<Image>

A responsive image, optionally wrapped in a link.

PropTypeDefaultDescription
srcrequiredurlImage URL (use an absolute, hosted URL).
altstringAlternative text (shown if the image can't load).
hrefurlMake the image a link to this URL.
widthnumberContainer (frame) width in px. The image scales to fill it; defaults to full content width.
heightnumberOptional frame height in px. By default the height adapts to the image's aspect ratio — set this to pin an explicit height.
objectFitenumcover | contain | fillHow the image fills the frame when a height is set: cover (crop), contain (letterbox), or fill (stretch). Client support varies.
backgroundColorcolorFrame background shown around the image when “contain” leaves gaps.
borderRadiusnumberCorner rounding in px.
alignenumleft | center | rightcenterHorizontal alignment.
border-widthnumberFrame border stroke width (px).
border-styleenumsolid | dashed | dottedFrame border stroke style.
border-colorcolorFrame border stroke color.

Interactive

<Button>

A call-to-action — a styled, padded link that looks like a button.

PropTypeDefaultDescription
hrefrequiredurl#Destination URL. {{variables}} are allowed.
alignenumleft | center | rightleftHorizontal alignment of the button.
background-colorcolor#18181bButton fill color.
text-colorcolor#ffffffLabel color.
border-radiusnumber9999Corner radius (px).
inner-padding-xnumber32Horizontal padding inside the button (px).
inner-padding-ynumber12Vertical padding inside the button (px).

Advanced

<Html>

There's no <Html> tag in code mode — any markup that isn't a known component (e.g. a <table> or <div>) automatically becomes an Html block and is emitted verbatim at render. Content passes through the 3-layer HTML sanitizer (scripts, event handlers, and javascript: URLs are stripped) both on save and at render. {{variables}} interpolate inside the markup. Prefer the built-in components when possible — raw HTML is on you to keep Outlook-safe.

No component-specific props beyond the common ones.

Common props

Every block accepts these in addition to its own props.

PropTypeDefaultDescription
margin-bottomnumber16Vertical space (px) below the block.
classNamestringTailwind-style utility classes compiled to inline CSS at render (e.g. "mt-4 text-zinc-500 sm:hidden"). Responsive/pseudo variants are hoisted into a <style> block. `class` and `tw` are accepted as aliases in code.
custom-csscssExtra inline CSS declarations merged onto the block's root element (e.g. "letter-spacing: 1px; opacity: 0.9"). Inlined so it survives every client.

Per-side spacing is also supported via padding and margin objects in the visual editor's inspector.

Layouts

Pre-built sections composed from the components above — headers, heroes, split columns, CTAs, and footers. Copy the code into a template, or insert them from the Layouts rail in the editor and restyle every block inline. They use only the primitives, so email client support is identical.

Header

Logo + links

Logo on the left, quick links on the right.

Hero

Simple hero

Big headline, supporting line, and a pill CTA — the transactional first screen.

Image hero

Full-width image above a headline, copy, and CTA — the announcement opener.

Status hero

Eyebrow label + headline + copy — confirmations and status updates.

Content

Card

A soft tinted card for grouped details — receipts, key facts, summaries.

Detail rows

Label/value lines in a tinted card — order summaries and invoices.

Verification code

A big spaced code inside a tinted card — OTP and verification emails.

Quote / callout

An indented callout for a quote or an important note.

Action feature

An emoji icon, a headline, and a button — perfect for calling out a specific setting or next step.

Announcement banner

A tinted callout card with an icon, headline, and link — great for product updates.

Article feature

An eyebrow label, headline, full-width image, and a link — for deep dives and sponsored content.

Columns

Split 50/50

Image beside copy — the classic feature split (stacks on mobile).

Two cards

Side-by-side bordered cards — check-in/checkout, before/after, plan A/B.

Three features

Three equal columns for a compact feature or stats strip.

Numbered steps

A stacked 1-2-3 list — onboarding and how-it-works flows.

CTA band

A filled band with a headline and a contrasting pill button.

Centered CTA

Just a headline and a pill button — minimal and direct.

Playground

Write email code and see the exact HTML our engine produces — live, in your browser, with nothing to install. It renders through the same pipeline the editor's exact preview and the send engine use.

Open the playground