Core concepts
Templates and roots
Every template has one root (Email, Page, Document or Canvas) that decides its output. Root props, your own components, and sharing content.
A template is JSX with one root, and the root decides what render() writes. This page covers the four roots, their props, and how to build templates out of your own components.
import { Email, Heading, Text } from "@hyper-elements/react";
const welcome = (
<Email width={600} previewText="Thanks for signing up" lang="en">
<Heading>Welcome</Heading>
<Text>Thanks for signing up.</Text>
</Email>
);
That is a whole email. A root stacks its children down its content width, centered in the window, so you don’t need a Row, a Column or a container to start.
The four roots
| Root | Output | Width |
|---|---|---|
Email |
Email-safe HTML | width, 600 by default |
Page |
A responsive web page | width, 1200 by default: the widest the content gets |
Document |
Print-ready HTML on pages of its size, and a PDF with @hyper-elements/react/pdf |
From its size, orientation and margin |
Canvas |
An image with @hyper-elements/react/image (and HTML from render(), to look at) |
width 1200 and height 630 by default |
A template has exactly one root. A <div> or any other HTML element around it is an error.
Root props
| Prop | Roots | What it does |
|---|---|---|
width |
Email, Page, Canvas | The content width in px |
height |
Canvas | The image’s height in px. What’s past it is cut |
previewText |
Shown after the subject in inbox lists, not in the email | |
description |
Page | The page’s meta description |
title |
all | The document’s <title>, shown in a browser tab and by some email clients |
lang |
all | The language, as a BCP 47 tag ("en", "ar") |
dir |
all | "ltr", "rtl" or "auto". Text starts on that side unless you align it |
fonts |
all | The font files the template loads. See Styles |
size |
Document | "A3", "A4", "A5", "Letter", "Legal" or "Tabloid", "A4" by default |
orientation |
Document | "portrait" or "landscape", "portrait" by default |
margin |
Document | Around each page’s content, 48px on each side by default (96px to the inch) |
A root’s width is a prop, not a style, and a root has no padding. Put padding on a Column (a card) or a Section (a band) inside it:
<Email width={640} style={{ backgroundColor: "#f4f4f5" }}>
<Column style={{ backgroundColor: "#ffffff", padding: 32 }}>
<Text>The card has the padding, not the root.</Text>
</Column>
</Email>
The root’s style takes the text styles every block inherits (color, fontFamily, fontSize, lineHeight and the rest), backgroundColor, gap and alignItems. A Canvas also takes padding and justifyContent. See Styles.
What goes inside
| Kind | Components |
|---|---|
| Layout | Section, Row, Column, Grid, Layer |
| Text | Heading (level 1 to 6), Text |
| Inside text | Span, Link, LineBreak, MergeTag |
| Blocks | Button, Image, Divider, List, Table, Video, Social, Menu, Svg (not in an Email), Html |
| Parts | ListItem in a List; TableRow and TableCell in a Table |
Shapes, in an Svg |
Rect, Circle, Ellipse, Line, Polygon, Path |
| Document parts | Document.Page, PageBreak, PageHeader, PageFooter, PageNumber |
Text is components, not HTML. Inside a Text, a Span styles a run, a Link links one, a LineBreak breaks the line and a MergeTag writes a merge tag:
<Text>
Order <Span style={{ fontWeight: 700 }}>#10482</Span> ships tomorrow.<LineBreak />
<Link href="https://example.com/orders/10482">Track it</Link>
</Text>
<b>, <br>, <div> and other HTML elements in a template are errors (jsx/markup). For markup no component says, there’s <Html>.
Your own components
A function that returns components is a component of your own. Write templates the way you write the rest of your React app:
import { Column, Email, Heading, Text } from "@hyper-elements/react";
function Footer({ company }: { company: string }) {
return <Text style={{ fontSize: 12, color: "#71717a", textAlign: "center" }}>{company}</Text>;
}
export function OrderShipped({ name, items }: { name: string; items: string[] }) {
return (
<Email previewText="Your order is on its way" style={{ gap: 16 }}>
<Column style={{ gap: 8 }}>
<Heading level={1}>On its way, {name}</Heading>
{items.map((item) => (
<Text key={item}>{item}</Text>
))}
</Column>
<Footer company="Acme Inc." />
</Email>
);
}
render() runs your components with React, so props, hooks, Context, Fragments, arrays and conditions work as React shows them.
A component that’s async or suspends is an error (jsx/async): render() doesn’t wait for it. Fetch your data first and pass it in as props.
Data you pass as children or props is escaped when the HTML is written, so a customer’s name can’t add markup.
Share content between roots
Write the content once, and give each output its root:
import { Button, Column, Document, Email, Heading, Page, Text, Span, render } from "@hyper-elements/react";
interface Invoice {
number: string;
customer: string;
total: string;
url: string;
}
function InvoiceContent({ invoice }: { invoice: Invoice }) {
return (
<Column style={{ backgroundColor: "#ffffff", padding: 24, gap: 16 }}>
<Heading level={1} style={{ fontSize: 28 }}>Payment receipt</Heading>
<Text>Invoice <Span style={{ fontWeight: 700 }}>{invoice.number}</Span></Text>
<Text>Thank you, {invoice.customer}. You paid {invoice.total}.</Text>
<Button href={invoice.url}>View invoice</Button>
</Column>
);
}
const invoice = { number: "INV-1001", customer: "Ada Lovelace", total: "$49.00", url: "https://example.com/invoices/INV-1001" };
const title = `Invoice ${invoice.number}`;
const email = await render(<Email title={title} previewText="Your payment receipt is ready."><InvoiceContent invoice={invoice} /></Email>);
const page = await render(<Page title={title} width={600}><InvoiceContent invoice={invoice} /></Page>);
const printed = await render(<Document title={title} size="A4" margin={48}><InvoiceContent invoice={invoice} /></Document>);
render() needs no mode: the root says what to write. email.html is email HTML, page.html a web page and printed.html print HTML. For the PDF itself, pass the same Document to renderPdf(). See PDF.
Shared content doesn’t mean identical pixels. Email is written as tables, a page with flexbox, and a document on pages of a fixed size. Each output’s page says what it does differently.
Next
- Layout: arrange blocks with Section, Row, Column, Grid and Layer.
- Custom components: named blocks with typed props.