@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` — it ships the component types, the parser, and the same `renderToHtml` renderer the send engine uses.

# npm
npm install @unitpost/email

# pnpm
pnpm add @unitpost/email

# yarn
yarn add @unitpost/email

# 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.

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 https://api.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.

<Section padding-x={24} padding-y={24}>
  <Text>Grouped content</Text>
</Section>
PropTypeDefaultDescription
background-colorcolorFill color behind the section.
padding-xnumber24Horizontal inner padding (px).
padding-ynumber24Vertical inner padding (px).

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

<Row column-gap={8}>
  <Column width={50}>
    <Text>Left</Text>
  </Column>
  <Column width={50}>
    <Text>Right</Text>
  </Column>
</Row>
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.

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

<Column width={50}>
  <Text>Column content</Text>
</Column>
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).

Content

<Heading>

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

<Heading level={2}>Welcome, {{first_name}}</Heading>
PropTypeDefaultDescription
levelenum1 | 2 | 3 | 42Heading level — controls size/weight.
alignenumleft | center | rightleftText alignment.
colorcolorText color.
font-familystringOverride the document font for this heading.

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

<Text>Hi {{first_name}}, thanks for signing up.</Text>
PropTypeDefaultDescription
alignenumleft | center | rightleftText alignment.
colorcolorText color.
font-sizenumber16Font size (px).
font-familystringOverride the document font for this block.

<Divider>

A thin horizontal rule to separate sections.

<Divider />
PropTypeDefaultDescription
colorcolor#e4e4e7Line color.

<Spacer>

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

<Spacer height={24} />
PropTypeDefaultDescription
heightrequirednumber24Gap height (px).

<Markdown>

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

<Markdown>**Welcome!** Here's _what's new_ this week. [See all](https://example.com)</Markdown>
PropTypeDefaultDescription
alignenumleft | center | rightleftAlignment.
colorcolorBase text color.
font-sizenumber16Base font size (px).

<Code>

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

<Code>npm install @unitpost/email</Code>
PropTypeDefaultDescription
background-colorcolor#f4f4f5Block background.
colorcolor#1a1a1aCode text color.

Media

<Image>

A responsive image, optionally wrapped in a link.

<Image src="https://example.com/logo.png" alt="Logo" width={120} />
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.

Interactive

<Button>

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

<Button href="https://example.com/verify?token={{token}}">Verify email</Button>
PropTypeDefaultDescription
hrefrequiredurl#Destination URL. {{variables}} are allowed.
alignenumleft | center | rightleftHorizontal alignment of the button.
background-colorcolor#2563ebButton fill color.
text-colorcolor#ffffffLabel color.
border-radiusnumber6Corner radius (px).
inner-padding-xnumber24Horizontal padding inside the button (px).
inner-padding-ynumber12Vertical padding inside the button (px).

Common props

Every block accepts these in addition to its own props.

PropTypeDefaultDescription
margin-bottomnumber16Vertical space (px) below the block.
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.