@hyper-elements/react 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/GUIDE.md ADDED
@@ -0,0 +1,176 @@
1
+ # Writing templates with the new components (`@hyper-elements/react`)
2
+
3
+ <!-- Written from the component definitions by src/primitives/reference.ts: don't edit by hand. Regenerate with UPDATE_REFERENCE=1 pnpm --filter @hyper-elements/react test reference -->
4
+
5
+ Everything needed to write a template, in short. Every prop's and style's details: [REFERENCE.md](./REFERENCE.md).
6
+
7
+ ## How a template is written
8
+
9
+ - Import from `@hyper-elements/react`. A template has one root: `Email`, `Page`, `Document` or `Canvas`.
10
+ - Styles go in `style={{ … }}`, never as props: `<Heading style={{ fontSize: 24, color: "#111827" }}>`. A number is px; `padding` takes `16`, `[12, 24]` or `"12px 24px"`. A box's text styles are inherited by what it holds.
11
+ - There are no margins: space between blocks is their parent's `gap` (`<Column style={{ gap: 16 }}>`), space inside a box its `padding`.
12
+ - A root, a `Section` and a `Column` stack their children. A `Row` puts its children side by side (each `Column` at its `style.width`, or an equal share) and stacks them on phones. `Grid columns={3}` makes rows of three. A `Section` is a band whose background spans the window; a `Column` with a background is a card.
13
+ - Text is components, not HTML: `Heading level={1}` and `Text` hold text; inside them `Span` styles a run, `Link href` links one, `LineBreak` breaks the line and `MergeTag name` writes a merge tag. No raw HTML (`<b>`, `<br>`, `<div>`) and no `html` prop.
14
+ - `Image src={{ src, width, height }}` (the file's size in px) with `alt`; `style.width` sets the size shown.
15
+ - Lists: `<List type="number">` of `ListItem`s. Tables: a `Table` of `TableRow`s (`header` on the first) of `TableCell`s, with `cellPadding` and `rowBorder` in the Table's `style`.
16
+ - Phones: `mobile={{ fontSize: 20 }}` (the styles listed under "Phones"), `hidden={{ mobile: true }}`. Hover: `hover={{ backgroundColor: "#374151" }}` (those under "Hover").
17
+ - `await render(element)` gives `{ html, text, diagnostics }`; a template with errors throws an `ElementsError` whose `diagnostics` give each problem's `code`, `message` and `path`. `validate(element)` runs the same checks without rendering. `outline(element)` prints the laid-out template (each block, its width, its text), to check the layout without reading the HTML.
18
+
19
+ ## Common mistakes
20
+
21
+ Each of these made an agent's first try fail:
22
+
23
+ - `<Email backgroundColor="#fff">` → `<Email style={{ backgroundColor: "#fff" }}>`. Styles never go as props.
24
+ - `<Email style={{ width: 600, padding: 24 }}>` → `<Email width={600}>`, with the padding on a `Column` (a card) or a `Section` (a band) inside it. A root's width is a prop, and a root has no padding.
25
+ - `items={[{ text: "Home", link: "/" }]}` → `items={[{ text: "Home", href: "/" }]}`. A `Menu`'s items are `{ text, href }`, a `Social`'s icons `{ name, href }`.
26
+ - `style={{ marginBottom: 16 }}` → `gap: 16` on the parent. There are no margins.
27
+ - `ColumnLayouts`, `layout`, `headingType`, `altText`, `html` are Unlayer Elements' props, not these: a `Row` of `Column`s, `level`, `alt`, and text as children.
28
+ - `<Image src="https://example.com/logo.png">` → `src={{ src: "https://example.com/logo.png", width: 240, height: 80 }}`, the file's size. Without it Outlook shows the image at its file's size, and Hyper Elements warns.
29
+
30
+ ## A whole template
31
+
32
+ ```tsx
33
+ import { Button, Column, Email, Heading, Image, Link, List, ListItem, MergeTag, Text, Row, Span, Table, TableCell, TableRow } from "@hyper-elements/react";
34
+
35
+ export default function Receipt() {
36
+ return (
37
+ <Email previewText="Your Acme receipt" style={{ backgroundColor: "#f4f4f5", fontFamily: "Inter, Arial, sans-serif", gap: 16 }}>
38
+ <Image src={{ src: "https://example.com/logo.png", width: 240, height: 80 }} alt="Acme" style={{ width: 120 }} />
39
+ <Column style={{ backgroundColor: "#ffffff", padding: 32, gap: 16, borderRadius: 8 }}>
40
+ <Heading level={1} style={{ fontSize: 24 }} mobile={{ fontSize: 20 }}>
41
+ Thanks for your order, <MergeTag name="first_name" />
42
+ </Heading>
43
+ <Text>
44
+ Order <Span style={{ fontWeight: 700 }}>#10482</Span> ships tomorrow. <Link href="https://example.com/orders/10482">Track it</Link>.
45
+ </Text>
46
+ <Table style={{ cellPadding: [8, 0], rowBorder: "1px solid #e4e4e7" }}>
47
+ <TableRow header>
48
+ <TableCell>Item</TableCell>
49
+ <TableCell style={{ textAlign: "right" }}>Price</TableCell>
50
+ </TableRow>
51
+ <TableRow>
52
+ <TableCell>Linen shirt</TableCell>
53
+ <TableCell style={{ textAlign: "right" }}>$48.00</TableCell>
54
+ </TableRow>
55
+ </Table>
56
+ <Row style={{ gap: 16 }}>
57
+ <Column>
58
+ <Text style={{ fontWeight: 700 }}>Shipping to</Text>
59
+ <Text>Ada Lovelace, London</Text>
60
+ </Column>
61
+ <Column>
62
+ <Text style={{ fontWeight: 700 }}>Next steps</Text>
63
+ <List type="number">
64
+ <ListItem>We pack it</ListItem>
65
+ <ListItem>You track it</ListItem>
66
+ </List>
67
+ </Column>
68
+ </Row>
69
+ <Button href="https://example.com/orders/10482" style={{ backgroundColor: "#111827", borderRadius: 6 }} hover={{ backgroundColor: "#374151" }}>
70
+ View your order
71
+ </Button>
72
+ </Column>
73
+ <Text style={{ fontSize: 12, color: "#71717a", textAlign: "center" }}>Acme Inc., 1 Market St, San Francisco</Text>
74
+ </Email>
75
+ );
76
+ }
77
+ ```
78
+
79
+ ## A Canvas as an image
80
+
81
+ `renderImage(element, options)` (`@hyper-elements/react/image`) writes a `Canvas` as a PNG (`format: "svg"` an SVG, `scale: 2` twice the pixels). Nothing is fetched: `fonts` gives each font file by the family the template names (TTF, OTF or WOFF, not WOFF2; one per weight and style used), `images` each image file by the `src` the template gives. It gives `{ image, width, height, diagnostics }` (what it drew otherwise than written: a weight not given, a hover); errors throw an `ElementsError`.
82
+
83
+ ```tsx
84
+ import { readFile } from "node:fs/promises";
85
+ import { Canvas, Heading, Image } from "@hyper-elements/react";
86
+ import { renderImage } from "@hyper-elements/react/image";
87
+
88
+ const { image, diagnostics } = await renderImage(
89
+ <Canvas width={1200} height={630} style={{ padding: 64, gap: 24, fontFamily: "Inter", textAlign: "center", justifyContent: "center" }}>
90
+ <Image src={{ src: "https://example.com/logo.png", width: 240, height: 80 }} alt="Acme" style={{ width: 120 }} />
91
+ <Heading level={1} style={{ fontSize: 64 }}>Launch week</Heading>
92
+ </Canvas>,
93
+ {
94
+ fonts: [{ family: "Inter", src: await readFile("Inter-Bold.ttf"), weight: 700 }],
95
+ images: { "https://example.com/logo.png": await readFile("logo.png") },
96
+ }
97
+ );
98
+ ```
99
+
100
+ ## Components
101
+
102
+ \* required. *text* styles: color, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textAlign, textTransform. *box* styles: width, maxWidth, padding, backgroundColor, border, borderRadius.
103
+
104
+ ### Roots
105
+
106
+ - **Email**: An email. Its children are stacked down the content width, centered in the window. Holds sections, layout, blocks. Props: width (a length (a number of px)), previewText (text), title (text), lang (text), dir (ltr|rtl|auto), fonts (fontFaces). Style: text, backgroundColor, gap, alignItems (start|center|end|stretch). Phones: fontSize, lineHeight, letterSpacing, textAlign, gap.
107
+ - **Page**: A web page. Its children are stacked down the content width, centered in the window. Holds sections, layout, blocks. Props: width (a length (a number of px)), description (text), title (text), lang (text), dir (ltr|rtl|auto), fonts (fontFaces). Style: text, backgroundColor, gap, alignItems (start|center|end|stretch). Phones: fontSize, lineHeight, letterSpacing, textAlign, gap.
108
+ - **Document**: A printed document: its children on pages of its size (Document.Page, or split by PageBreaks), a PageHeader and a PageFooter on each. Renders to print HTML, and to PDF with @hyper-elements/react/pdf. Holds sections, layout, blocks, Document.Page, PageBreak, PageHeader, PageFooter. Props: size (A3|A4|A5|Letter|Legal|Tabloid), orientation (portrait|landscape), margin (insets), title (text), lang (text), dir (ltr|rtl|auto), fonts (fontFaces). Style: text, backgroundColor, gap, alignItems (start|center|end|stretch). Phones: fontSize, lineHeight, letterSpacing, textAlign, gap.
109
+ - **Canvas**: An image of a fixed size (a social card, a banner): its children laid out in it as on a page. Written as PNG or SVG with @hyper-elements/react/image. Holds sections, layout, blocks. Props: width (a length (a number of px)), height (a length (a number of px)), title (text), lang (text), dir (ltr|rtl|auto), fonts (fontFaces). Style: text, backgroundColor, gap, alignItems (start|center|end|stretch), padding, justifyContent (start|center|end|space-between). Phones: fontSize, lineHeight, letterSpacing, textAlign, gap, padding.
110
+
111
+ ### Sections and layout
112
+
113
+ - **Section**: A band across the whole window, directly in an Email or Page: its background spans the window, its children are stacked at the content width. Holds layout, blocks. Style: text, backgroundColor, padding, border, gap, alignItems (start|center|end|stretch). Phones: fontSize, lineHeight, letterSpacing, textAlign, padding, border, gap.
114
+ - **Row**: Lays its children out side by side. A child takes its width, or a share of what's left (its flex, 1 by default). Holds layout, blocks. Props: stackOnMobile (true or false), dir (ltr|rtl|auto). Style: text, box, minWidth, height, overflow (visible|hidden), gap, alignItems (start|center|end|stretch), justifyContent (start|center|end|space-between), flexWrap (nowrap|wrap|words), flex. Phones: fontSize, lineHeight, letterSpacing, textAlign, width, maxWidth, padding, border, gap.
115
+ - **Column**: Stacks its children vertically, with its own spacing and box. Holds layout, blocks. Style: text, box, minWidth, height, overflow (visible|hidden), gap, alignItems (start|center|end|stretch), justifyContent (start|center|end|space-between), flex. Phones: fontSize, lineHeight, letterSpacing, textAlign, width, maxWidth, padding, border, gap.
116
+ - **Grid**: Its children in columns of equal width, row after row; on phones, one under another. Holds layout, blocks. Props: columns (an integer from 1 to 12), stackOnMobile (true or false). Style: text, box, minWidth, height, overflow (visible|hidden), gap, rowGap, alignItems (start|center|end|stretch), flex. Phones: fontSize, lineHeight, letterSpacing, textAlign, width, maxWidth, padding, border, gap, rowGap.
117
+ - **Layer**: Its children over each other, in one box as tall as the tallest: an image with words on it. In an email, an Image with one child over it. Holds layout, blocks. Style: text, width, minWidth, maxWidth, backgroundColor, border, borderRadius, height, alignItems (start|center|end|stretch), justifyItems (start|center|end|stretch), flex. Phones: fontSize, lineHeight, letterSpacing, textAlign, width, maxWidth, border.
118
+
119
+ ### Blocks
120
+
121
+ - **Svg**: Shapes (Rect, Circle, Ellipse, Line, Polygon, Path) drawn in a box of its own: its width and height, and the coordinates its viewBox gives them. Holds shapes. Props: width* (a number from 0), height* (a number from 0), viewBox (text), title (text). Style: padding, backgroundColor, border, borderRadius, flex. Phones: padding, border.
122
+ - **Text**: A paragraph of text. Holds text, inline, Image. Props: dir (ltr|rtl|auto). Style: text, box, textDecoration (none|underline|line-through), flex. Phones: fontSize, lineHeight, letterSpacing, textAlign, width, maxWidth, padding, border.
123
+ - **Heading**: A heading, from level 1 (the page's title) to 6. Holds text, inline. Props: level (1|2|3|4|5|6), dir (ltr|rtl|auto). Style: text, box, textDecoration (none|underline|line-through), flex. Phones: fontSize, lineHeight, letterSpacing, textAlign, width, maxWidth, padding, border.
124
+ - **Button**: A link drawn as a button. Holds text, Span, MergeTag, LineBreak. Props: href* (link), title (text), aria-label (text), attrs (attributes). Style: text, box, textDecoration (none|underline|line-through), boxSizing (border-box|content-box), alignSelf (start|center|end), flex. Hover: color, backgroundColor. Phones: fontSize, lineHeight, letterSpacing, textAlign, width, maxWidth, padding, border, alignSelf.
125
+ - **Image**: An image in the content. Without a width it takes its own size, no wider than its parent. Props: src* (asset), alt (text), title (text). Style: width, maxWidth, height, border, borderRadius, flex, alignSelf (start|center|end), verticalAlign (top|middle). Phones: width, maxWidth, border, alignSelf.
126
+ - **Divider**: A horizontal line. Style: borderTop, width, padding. Phones: width, padding.
127
+ - **List**: A bulleted or numbered list of ListItems. Its markers sit in a column of their own (indent) before the items. Holds ListItem. Props: type (bullet|number), start (an integer from 0). Style: text, width, maxWidth, padding, backgroundColor, border, gap, indent, flex. Phones: fontSize, lineHeight, letterSpacing, textAlign, width, maxWidth, padding, border, gap.
128
+ - **Table**: A table of data: TableRows of TableCells. Columns take their cells' widths; the rest share what's left. Holds TableRow. Style: text, width, maxWidth, backgroundColor, border, cellPadding, rowBorder, borderCollapse (separate|collapse), flex. Phones: fontSize, lineHeight, letterSpacing, textAlign, width, maxWidth, border.
129
+ - **Video**: A YouTube or Vimeo video: its player on a page; in an email (which can't play one), its thumbnail, linked to it. Props: src* (video), thumbnail (asset), title (text). Style: width, maxWidth, flex. Phones: width, maxWidth.
130
+ - **Social**: Links to social profiles: a line of the networks' icons, each linked to its profile. Props: icons* (a list of icons, each { name, href }), iconType (circle|rounded|squared), size (an integer from 1). Style: gap, justifyContent (start|center|end), flex. Phones: gap.
131
+ - **Menu**: A line of links (a site's sections, a footer's), onto more lines where they don't fit, with a separator between them on desktop; or links down a column. Props: items* (a list of items, each { text, href }), separator (text), layout (horizontal|vertical). Style: color, fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, textTransform (none|uppercase|lowercase|capitalize), separatorColor, padding, justifyContent (start|center|end), flex. Phones: fontSize, lineHeight, letterSpacing.
132
+ - **Html**: Markup written as it is, for what no component says (the builder's HTML block): in an email and on a page as given, nothing in it checked or escaped. A Document's PDF and a Canvas's image draw its plain text. Props: html* (text), text (text).
133
+
134
+ ### Inline
135
+
136
+ - **PageNumber**: In a Document's text: the number of the page it's on, or (total) how many pages there are. Props: total (true or false).
137
+ - **Span**: Styled text inside text; raised or lowered (superscript, subscript) with verticalAlign. Holds text, inline. Style: color, fontFamily, fontSize, fontWeight, fontStyle (normal|italic), letterSpacing, textTransform (none|uppercase|lowercase|capitalize), textDecoration (none|underline|line-through), backgroundColor, verticalAlign (baseline|super|sub). Phones: fontSize, letterSpacing.
138
+ - **Link**: A link: text inside text, or around an Image. Holds text, inline, Image. Props: href* (link), title (text), aria-label (text), attrs (attributes). Style: color, fontFamily, fontSize, fontWeight, fontStyle (normal|italic), letterSpacing, textTransform (none|uppercase|lowercase|capitalize), textDecoration (none|underline|line-through), backgroundColor, flex. Hover: color, textDecoration, backgroundColor. Phones: fontSize, letterSpacing.
139
+ - **LineBreak**: Starts a new line inside text.
140
+ - **MergeTag**: A value the sending service fills in (a name, a link), written in the service's own syntax. Props: name* (text).
141
+
142
+ ### Parts
143
+
144
+ - **Document.Page**: One page of a Document (Document.Page in JSX): its children stacked down it, inside the Document's margins. Holds sections, layout, blocks. Style: text, backgroundColor, gap, alignItems (start|center|end|stretch). Phones: fontSize, lineHeight, letterSpacing, textAlign, gap.
145
+ - **PageBreak**: In a Document, where a page ends: what follows starts the next page.
146
+ - **PageHeader**: In a Document: drawn at the top of every page, inside its margins. Holds layout, blocks. Style: text, padding, backgroundColor, border, gap, alignItems (start|center|end|stretch). Phones: fontSize, lineHeight, letterSpacing, textAlign, padding, border, gap.
147
+ - **PageFooter**: In a Document: drawn at the bottom of every page, inside its margins. Holds layout, blocks. Style: text, padding, backgroundColor, border, gap, alignItems (start|center|end|stretch). Phones: fontSize, lineHeight, letterSpacing, textAlign, padding, border, gap.
148
+ - **ListItem**: An item of a List: text, and a List of its own to nest one. Holds text, inline, List. Props: dir (ltr|rtl|auto). Style: text, textDecoration (none|underline|line-through). Phones: fontSize, lineHeight, letterSpacing, textAlign.
149
+ - **TableRow**: A row of a Table. A header row's cells are header cells, in bold. Holds TableCell. Props: header (true or false), footer (true or false). Style: text, backgroundColor, height. Phones: fontSize, lineHeight, letterSpacing, textAlign.
150
+ - **TableCell**: A cell of a TableRow: text, and images. Its width sets its column's. Holds text, inline, Image. Props: colSpan (an integer from 1), dir (ltr|rtl|auto). Style: text, textDecoration (none|underline|line-through), padding, backgroundColor, border, width, verticalAlign (top|middle|bottom). Phones: fontSize, lineHeight, letterSpacing, textAlign, padding, border.
151
+
152
+ ### Shapes
153
+
154
+ - **Rect**: A rectangle at x, y in its Svg, rounded by rx. Props: x (a number), y (a number), width* (a number from 0), height* (a number from 0), rx (a number from 0). Style: fill, stroke, strokeWidth, opacity.
155
+ - **Circle**: A circle centered at cx, cy in its Svg. Props: cx (a number), cy (a number), r* (a number from 0). Style: fill, stroke, strokeWidth, opacity.
156
+ - **Ellipse**: An ellipse centered at cx, cy in its Svg. Props: cx (a number), cy (a number), rx* (a number from 0), ry* (a number from 0). Style: fill, stroke, strokeWidth, opacity.
157
+ - **Line**: A straight line from x1, y1 to x2, y2 in its Svg (drawn by its stroke). Props: x1 (a number), y1 (a number), x2 (a number), y2 (a number). Style: fill, stroke, strokeWidth, opacity.
158
+ - **Polygon**: A closed shape through its points ("0,0 40,0 20,30"), in its Svg. Props: points* (text). Style: fill, stroke, strokeWidth, opacity.
159
+ - **Path**: A shape drawn by an SVG path (d), in its Svg. Props: d* (text). Style: fill, stroke, strokeWidth, opacity.
160
+
161
+ ## Values
162
+
163
+ - *fontFaces*: a list of { family, src, format, weight, style }
164
+ - *color*: a color ("#0879a1", "rgb(…)", "rgba(…)" or a CSS name)
165
+ - *fontFamily*: a font stack, as CSS writes it ("Inter, Arial, sans-serif")
166
+ - *fontWeight*: 100 to 900 in hundreds, "normal" or "bold"
167
+ - *lineHeight*: a multiple of the font size (1.5), a percentage ("150%"), px ("24px") or "normal" (the font's own)
168
+ - *insets*: px for all sides, "8px 16px" (1 to 4 values), [v, h] or [t, r, b, l], or { top, right, bottom, left }
169
+ - *border*: "1px solid #ccc", { width, style, color } for all sides, or { top, right, bottom, left } with one of those each
170
+ - *radius*: px for every corner, [topLeft, topRight, bottomRight, bottomLeft], or { topLeft, topRight, bottomRight, bottomLeft }
171
+ - *flex*: a share of the free width (0 or more), or "none"
172
+ - *link*: a URL (http, https, mailto, tel, a path, or a merge tag) or { href, target }
173
+ - *attributes*: attributes by name, each a text value (no on… handlers, href or style)
174
+ - *asset*: an image URL (http, https or a data:image URI), or { src, width, height }
175
+ - *line*: "1px solid #ccc" or { width, style, color }
176
+ - *video*: a YouTube or Vimeo video's https link
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Unlayer, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,372 @@
1
- # Temporary Holding Version
1
+ # @hyper-elements/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React components for transactional emails, email templates, landing pages, invoices, reports and documents. One template renders to email-safe HTML, a responsive web page, print-ready HTML, a PDF or an image, on your own server, with no network calls.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @hyper-elements/react
9
+ # or
10
+ pnpm add @hyper-elements/react
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ Import from `@hyper-elements/react`. A template has one root, its styles go in `style={{ }}`, and the space between blocks is their parent's `gap`.
16
+
17
+ ```tsx
18
+ import { Button, Column, Email, Heading, Image, Link, List, ListItem, MergeTag, Text, Row, Span, Table, TableCell, TableRow } from "@hyper-elements/react";
19
+
20
+ export default function Receipt() {
21
+ return (
22
+ <Email previewText="Your Acme receipt" style={{ backgroundColor: "#f4f4f5", fontFamily: "Inter, Arial, sans-serif", gap: 16 }}>
23
+ <Image src={{ src: "https://example.com/logo.png", width: 240, height: 80 }} alt="Acme" style={{ width: 120 }} />
24
+ <Column style={{ backgroundColor: "#ffffff", padding: 32, gap: 16, borderRadius: 8 }}>
25
+ <Heading level={1} style={{ fontSize: 24 }} mobile={{ fontSize: 20 }}>
26
+ Thanks for your order, <MergeTag name="first_name" />
27
+ </Heading>
28
+ <Text>
29
+ Order <Span style={{ fontWeight: 700 }}>#10482</Span> ships tomorrow. <Link href="https://example.com/orders/10482">Track it</Link>.
30
+ </Text>
31
+ <Table style={{ cellPadding: [8, 0], rowBorder: "1px solid #e4e4e7" }}>
32
+ <TableRow header>
33
+ <TableCell>Item</TableCell>
34
+ <TableCell style={{ textAlign: "right" }}>Price</TableCell>
35
+ </TableRow>
36
+ <TableRow>
37
+ <TableCell>Linen shirt</TableCell>
38
+ <TableCell style={{ textAlign: "right" }}>$48.00</TableCell>
39
+ </TableRow>
40
+ </Table>
41
+ <Row style={{ gap: 16 }}>
42
+ <Column>
43
+ <Text style={{ fontWeight: 700 }}>Shipping to</Text>
44
+ <Text>Ada Lovelace, London</Text>
45
+ </Column>
46
+ <Column>
47
+ <Text style={{ fontWeight: 700 }}>Next steps</Text>
48
+ <List type="number">
49
+ <ListItem>We pack it</ListItem>
50
+ <ListItem>You track it</ListItem>
51
+ </List>
52
+ </Column>
53
+ </Row>
54
+ <Button href="https://example.com/orders/10482" style={{ backgroundColor: "#111827", borderRadius: 6 }} hover={{ backgroundColor: "#374151" }}>
55
+ View your order
56
+ </Button>
57
+ </Column>
58
+ <Text style={{ fontSize: 12, color: "#71717a", textAlign: "center" }}>Acme Inc., 1 Market St, San Francisco</Text>
59
+ </Email>
60
+ );
61
+ }
62
+ ```
63
+
64
+ ```tsx
65
+ import { outline, render, validate } from "@hyper-elements/react";
66
+
67
+ const { html, text, diagnostics } = await render(<Receipt />); // warnings in diagnostics; errors throw an ElementsError
68
+ const { valid } = validate(<Receipt />); // the same checks, without rendering
69
+ console.log(outline(<Receipt />)); // the laid-out template as text: each block, its width, its words
70
+ ```
71
+
72
+ To write a template, read [GUIDE.md](./GUIDE.md): how a template is written, and every component on a line. [REFERENCE.md](./REFERENCE.md) has every prop and style in full. Both are written from the components' definitions.
73
+
74
+ Upgrading from Unlayer Elements? See [Moving from Unlayer Elements](#moving-from-unlayer-elements).
75
+
76
+ ## Using with an AI agent
77
+
78
+ **With an AI agent:** in Claude Code, add this line to your project's `CLAUDE.md`, and every session starts out knowing these components:
79
+
80
+ ```
81
+ @node_modules/@hyper-elements/react/GUIDE.md
82
+ ```
83
+
84
+ For Codex, Cursor and other agents, add to `AGENTS.md` (or a rule): "Before writing a Hyper Elements template, read node_modules/@hyper-elements/react/GUIDE.md."
85
+
86
+ Or install the same guide as an agent skill: `npx skills add hyper-elements/hyper-elements`.
87
+
88
+ ## Structure
89
+
90
+ ```
91
+ <Email> ← the root: Email, Page, Document or Canvas
92
+ <Section> ← a band whose background spans the window (optional)
93
+ <Row> ← its children side by side, stacked on phones
94
+ <Column> ← its children stacked
95
+ <Heading />
96
+ <Text />
97
+ <Button />
98
+ </Column>
99
+ </Row>
100
+ </Section>
101
+ </Email>
102
+ ```
103
+
104
+ A root, a `Section` and a `Column` stack what they hold, so a block doesn't need a `Row` around it. There are no margins: space between blocks is their parent's `gap`, and space inside a box is its `padding`.
105
+
106
+ ## Components
107
+
108
+ ### Roots
109
+
110
+ | Component | Renders to |
111
+ |-----------|-------------|
112
+ | `<Email>` | Email-safe HTML: tables, with Outlook's markup in conditional comments |
113
+ | `<Page>` | A responsive web page |
114
+ | `<Document>` | Print-ready HTML on pages of its size, and a PDF with `@hyper-elements/react/pdf` |
115
+ | `<Canvas>` | An image of a fixed size, with `@hyper-elements/react/image` |
116
+
117
+ ### Layout
118
+
119
+ | Component | Description |
120
+ |-----------|-------------|
121
+ | `<Section>` | A band across the whole window, directly in an Email or Page |
122
+ | `<Row>` | Its children side by side; stacked on phones |
123
+ | `<Column>` | Its children stacked, with its own spacing and box; with a background, a card |
124
+ | `<Grid>` | Its children in columns of equal width, row after row |
125
+ | `<Layer>` | Its children over each other: an image with words on it |
126
+
127
+ ### Content
128
+
129
+ | Component | Description |
130
+ |-----------|-------------|
131
+ | `<Heading>` | A heading, `level` 1 to 6 |
132
+ | `<Text>` | A block of text |
133
+ | `<Span>`, `<Link>`, `<LineBreak>`, `<MergeTag>` | Inside text: a styled run, a link, a new line, a value the sending service fills in |
134
+ | `<Button>` | A link drawn as a button, with its colors on hover |
135
+ | `<Image>` | An image; `src` has the file's size, `style.width` the size shown |
136
+ | `<Divider>` | A horizontal line |
137
+ | `<List>`, `<ListItem>` | A bulleted or numbered list |
138
+ | `<Table>`, `<TableRow>`, `<TableCell>` | A table of data |
139
+ | `<Video>` | A YouTube or Vimeo video: its player on a page, its linked thumbnail in an email |
140
+ | `<Social>` | A line of social network icons, each a link |
141
+ | `<Menu>` | A line or column of links |
142
+ | `<Svg>` and its shapes | Shapes drawn in a box of their own |
143
+ | `<Html>` | Markup written as it is, for what no component says |
144
+ | `<PageBreak>`, `<PageHeader>`, `<PageFooter>`, `<PageNumber>` | In a Document: where a page ends, what's on every page, and page numbers |
145
+
146
+ ## Columns
147
+
148
+ A `Column` in a `Row` takes its `width`, or a share of what's left.
149
+
150
+ ```tsx
151
+ import { Column, Grid, Row } from "@hyper-elements/react";
152
+
153
+ // Equal shares.
154
+ <Row style={{ gap: 16 }}>
155
+ <Column>{/* content */}</Column>
156
+ <Column>{/* content */}</Column>
157
+ </Row>
158
+
159
+ // A set width, and what's left.
160
+ <Row style={{ gap: 16 }}>
161
+ <Column style={{ width: 180 }}>{/* content */}</Column>
162
+ <Column>{/* content */}</Column>
163
+ </Row>
164
+
165
+ // Shares of two to one.
166
+ <Row style={{ gap: 16 }}>
167
+ <Column style={{ flex: 2 }}>{/* content */}</Column>
168
+ <Column>{/* content */}</Column>
169
+ </Row>
170
+
171
+ // Side by side on phones too.
172
+ <Row stackOnMobile={false}>
173
+ <Column>{/* content */}</Column>
174
+ <Column>{/* content */}</Column>
175
+ </Row>
176
+
177
+ // Rows of three, one under another on phones.
178
+ <Grid columns={3} style={{ gap: 16 }}>
179
+ <Column>{/* content */}</Column>
180
+ <Column>{/* content */}</Column>
181
+ <Column>{/* content */}</Column>
182
+ </Grid>
183
+ ```
184
+
185
+ ## Rendering
186
+
187
+ ```tsx
188
+ const { html, text, head, body, bodyStyle, diagnostics } = await render(<Receipt />);
189
+ ```
190
+
191
+ - `html` is the whole document, from `<!DOCTYPE html>`, with no React hydration markers and no client-side JavaScript.
192
+ - `text` is the plain-text version.
193
+ - `head`, `body` and `bodyStyle` are for embedding the output in a page of your own.
194
+ - `diagnostics` holds warnings and notes. A template with errors throws an `ElementsError`, whose `diagnostics` give each problem's `code`, `message` and `path`.
195
+
196
+ The options are `pretty` (the HTML indented, for reading it), `mergeTag` (how a merge tag is written, by default `{{name}}`) and `classPrefix` (what every class name starts with, for HTML embedded where other CSS is).
197
+
198
+ `validate()` runs the same checks without rendering, and `outline()` prints the laid-out template, to check the layout without reading the HTML.
199
+
200
+ ## One template, every output
201
+
202
+ Share the content, and give each output its root:
203
+
204
+ ```tsx
205
+ import { Button, Column, Document, Email, Heading, Page, Text, render } from "@hyper-elements/react";
206
+
207
+ const Invoice = () => (
208
+ <Column style={{ gap: 12 }}>
209
+ <Heading level={1} style={{ fontSize: 24 }}>Invoice 2705</Heading>
210
+ <Text>Thank you for your order.</Text>
211
+ <Button href="https://example.com/invoices/2705" style={{ backgroundColor: "#111827" }}>View it online</Button>
212
+ </Column>
213
+ );
214
+
215
+ const email = await render(<Email><Invoice /></Email>);
216
+ const page = await render(<Page><Invoice /></Page>);
217
+ const printed = await render(<Document size="A4" margin={48}><Invoice /></Document>);
218
+ ```
219
+
220
+ `printed.html` is print-ready HTML. For PDF files, [`@hyper-elements/react/pdf`](https://github.com/hyper-elements/hyper-elements/blob/main/packages/react/docs/render-pdf.md) writes a `Document` with no browser, and [`@hyper-elements/react/image`](https://github.com/hyper-elements/hyper-elements/blob/main/packages/react/docs/render-image.md) writes a `Canvas` as PNG or SVG.
221
+
222
+ ## Phones and hover
223
+
224
+ `mobile` holds a component's styles on phones, `hidden` shows it on one size only, and `hover` holds its styles under the pointer. Each component's list is in [GUIDE.md](./GUIDE.md).
225
+
226
+ ```tsx
227
+ <Heading style={{ fontSize: 32 }} mobile={{ fontSize: 24, textAlign: "center" }}>Welcome</Heading>
228
+ <Text hidden={{ mobile: true }}>Desktop detail</Text>
229
+ <Text hidden={{ desktop: true }}>Phone detail</Text>
230
+ <Button href="https://example.com" style={{ backgroundColor: "#111827" }} hover={{ backgroundColor: "#374151" }}>Buy</Button>
231
+ ```
232
+
233
+ ## Common design patterns
234
+
235
+ ### Header with a logo
236
+
237
+ ```tsx
238
+ <Section style={{ backgroundColor: "#ffffff", padding: [24, 40] }}>
239
+ <Image src={{ src: "https://example.com/logo.png", width: 240, height: 80 }} alt="Logo" style={{ width: 120 }} />
240
+ </Section>
241
+ ```
242
+
243
+ ### Accent bar
244
+
245
+ ```tsx
246
+ <Divider style={{ borderTop: "3px solid #4f46e5" }} />
247
+ ```
248
+
249
+ ### Feature grid
250
+
251
+ ```tsx
252
+ <Grid columns={2} style={{ backgroundColor: "#ffffff", padding: [24, 40], gap: 24 }}>
253
+ <Column style={{ gap: 4 }}>
254
+ <Heading level={3} style={{ fontSize: 16, fontWeight: 600 }}>Feature 1</Heading>
255
+ <Text style={{ fontSize: 13, color: "#71717a" }}>Description of the feature.</Text>
256
+ </Column>
257
+ <Column style={{ gap: 4 }}>
258
+ <Heading level={3} style={{ fontSize: 16, fontWeight: 600 }}>Feature 2</Heading>
259
+ <Text style={{ fontSize: 13, color: "#71717a" }}>Description of the feature.</Text>
260
+ </Column>
261
+ </Grid>
262
+ ```
263
+
264
+ ### Metric cards
265
+
266
+ ```tsx
267
+ <Row style={{ gap: 16 }}>
268
+ <Column style={{ backgroundColor: "#ffffff", padding: 16, borderRadius: 8, textAlign: "center" }}>
269
+ <Heading level={2} style={{ fontSize: 28, fontWeight: 700, color: "#0f172a" }}>1.2M</Heading>
270
+ <Text style={{ fontSize: 12, color: "#94a3b8" }}>API calls</Text>
271
+ </Column>
272
+ {/* One Column for each metric */}
273
+ </Row>
274
+ ```
275
+
276
+ ### Product card
277
+
278
+ ```tsx
279
+ <Row style={{ backgroundColor: "#ffffff", padding: [20, 40], gap: 16 }}>
280
+ <Column style={{ width: 160 }}>
281
+ <Image src={{ src: "https://example.com/product.jpg", width: 320, height: 320 }} alt="Product" style={{ width: "100%" }} />
282
+ </Column>
283
+ <Column style={{ gap: 4 }}>
284
+ <Heading level={3} style={{ fontSize: 16, fontWeight: 600 }}>Product name</Heading>
285
+ <Text style={{ fontSize: 13, color: "#a1a1aa" }}>Matte white, medium</Text>
286
+ <Text style={{ fontSize: 16, fontWeight: 700 }}>$89.00</Text>
287
+ </Column>
288
+ </Row>
289
+ ```
290
+
291
+ ### Footer
292
+
293
+ ```tsx
294
+ <Section style={{ padding: [20, 40, 40, 40] }}>
295
+ <Text style={{ fontSize: 12, color: "#a1a1aa", textAlign: "center" }}>Company name, City, State</Text>
296
+ </Section>
297
+ ```
298
+
299
+ ## Your own components
300
+
301
+ A function that returns components is a component of your own, as in [One template, every output](#one-template-every-output). For a named block with typed props and its own editors, use `component()`:
302
+
303
+ ```tsx
304
+ import { Button, Column, Heading, Image, component, prop } from "@hyper-elements/react";
305
+
306
+ const ProductCard = component({
307
+ type: "acme.ProductCard",
308
+ label: "Product card",
309
+ props: {
310
+ name: prop.text({ label: "Name", required: true }),
311
+ photo: prop.image({ label: "Photo", required: true }),
312
+ href: prop.link({ label: "Link", required: true }),
313
+ accent: prop.color({ label: "Accent", default: "#0879a1" }),
314
+ },
315
+ render: ({ name, photo, href, accent }) => (
316
+ <Column style={{ gap: 8 }}>
317
+ <Image src={photo} alt={name} />
318
+ <Heading level={3}>{name}</Heading>
319
+ <Button href={href} style={{ backgroundColor: accent }}>Buy</Button>
320
+ </Column>
321
+ ),
322
+ });
323
+ ```
324
+
325
+ For markup no component says, `<Html html="…" />` writes it as it is, in an email and on a page, with nothing in it checked or escaped. Put only markup you wrote there.
326
+
327
+ ## To the Unlayer builder and back
328
+
329
+ `@hyper-elements/react/builder`: `toDesign()` writes a template as the builder's design JSON, for `loadDesign()`; `fromDesign()` reads a design (made by `toDesign()`, edited in the builder or made there) back as a template, as an element to render and as JSX source.
330
+
331
+ ```tsx
332
+ import { fromDesign, toDesign } from "@hyper-elements/react/builder";
333
+
334
+ const { design, report } = toDesign(<Receipt />); // report: what the builder can't hold as it is, one line each
335
+ // …the design is edited in the builder, and saved…
336
+ const { element, code, report: read } = fromDesign(saved, { displayMode: "email" }); // "web" a Page, "document" a Document
337
+ ```
338
+
339
+ The design holds no Hyper Elements data. A style or prop a builder tool has no field for is a hidden property of that tool, kept in the design under its own name and drawn by Hyper Elements, with no editor in the builder yet. What the builder can't arrange comes back with the same look, flatter: a Section around several Rows as a Section each, a box's padding on its blocks, a Grid as Rows of Columns. What no tool shows (a Layer, an Svg, a Table, a box nested in a column, your own `component()`) goes as an HTML block of the new engine's markup, and comes back as an `Html` block, as the builder's own HTML blocks do. A design's markup comes back as it is: where designs can hold untrusted content, pass your sanitizer, `fromDesign(saved, { toSafeHtml })`. A line height goes in the builder's field as a whole percentage, as browsers draw it, so `24px` at 13px is `185%`, a twentieth of a pixel taller there. The builder has no page numbers yet: a `PageNumber` goes as its token, which the editor's PDF prints as it is, and `report` says so.
340
+
341
+ ## Task guides and runnable examples
342
+
343
+ - [Transactional email in React](https://github.com/hyper-elements/hyper-elements/blob/main/packages/react/docs/transactional-email.md): render HTML and plain text, then send a receipt.
344
+ - [Resend, AWS SES, Postmark, SendGrid, and Nodemailer](https://github.com/hyper-elements/hyper-elements/blob/main/packages/react/docs/email-providers.md): complete provider integrations.
345
+ - [One React template to email, web, and PDF](https://github.com/hyper-elements/hyper-elements/blob/main/packages/react/docs/one-template-email-web-pdf.md): shared invoice content with output-specific roots.
346
+ - [React invoice PDF with Playwright](https://github.com/hyper-elements/hyper-elements/blob/main/packages/react/docs/react-to-pdf.md): generate actual PDF bytes from document HTML.
347
+ - [React to Unlayer visual editing](https://github.com/hyper-elements/hyper-elements/blob/main/packages/react/docs/visual-editing.md): `toDesign()`, editor loading, export, and what the builder can hold.
348
+
349
+ See [when to choose Hyper Elements](https://github.com/hyper-elements/hyper-elements/blob/main/packages/react/docs/README.md) and the [runnable example](https://github.com/hyper-elements/hyper-elements/tree/main/examples/content-workflows).
350
+
351
+ ## Moving from Unlayer Elements
352
+
353
+ Unlayer Elements (`@unlayer/react-elements`) is the old package. Its components (`Body`, `ColumnLayouts`, `renderToHtml()` and the rest) aren't in this one. The codemod moves templates written with them:
354
+
355
+ ```bash
356
+ npx @hyper-elements/migrate src/emails --from unlayer-elements # templates written with Unlayer Elements
357
+ npx @hyper-elements/migrate src/emails # React Email templates
358
+ ```
359
+
360
+ It lists what it would change, and `--write` replaces the files (only those git can restore). Then install `@hyper-elements/react`, and remove `@unlayer/react-elements` once nothing imports it. What it can't move is listed, and that file is left as it was to rewrite by hand. See the [migration guide](https://github.com/hyper-elements/hyper-elements/blob/main/packages/react/docs/migrating-from-unlayer-elements.md) and [`@hyper-elements/migrate`](https://github.com/hyper-elements/hyper-elements/tree/main/packages/migrate).
361
+
362
+ ## Development
363
+
364
+ ```bash
365
+ pnpm build # Build the package
366
+ pnpm test # Run tests
367
+ pnpm storybook # Launch Storybook
368
+ ```
369
+
370
+ ## License
371
+
372
+ MIT