Core concepts
Diagnostics
What validate() and render() report: ElementsError, severities, each diagnostic's code, message and path, the codes you'll meet, and checks in CI.
Every check Hyper Elements runs ends up as a diagnostic: a stable code, a severity, a message that says what to do instead, and the path to the value. Errors stop a render. Warnings and notes tell you what an output can’t show as you asked.
import { Email, Text, validate } from "@hyper-elements/react";
const { valid, diagnostics } = validate(
<Email>
<Text style={{ marginBottom: 16 }}>Hi</Text>
</Email>
);
// valid: false
// diagnostics[0].code: "style/unknown"
// diagnostics[0].path: "root.children[0].style.marginBottom"
// diagnostics[0].message: "\"marginBottom\" isn't a style a Text takes: use the parent's gap, or padding. …"
Props and styles are typed from each component’s definition, so in TypeScript a margin like this one is a type error before it’s a diagnostic. validate() catches it at runtime too, with the path and the fix, for JavaScript, generated templates and agents.
A diagnostic
interface Diagnostic {
code: string; // stable, for tools to match on: "style/unknown"
severity: "error" | "warning" | "info";
message: string; // what's wrong, and what to do instead
path: string; // where: "root.children[1].style.padding"
nodeId?: string; // the node's id, where it has one
}
The path walks the template from the root: root.children[2].style.justifyContent is the third child’s justifyContent style.
Severities
| Severity | Means | render() |
validate() |
|---|---|---|---|
error |
The template can’t be rendered as written | Rejects with an ElementsError |
valid: false |
warning |
It renders, but not as you asked: an output can’t show something, a width doesn’t fit | In diagnostics |
valid: true |
info |
A note: shown as asked, except somewhere (classic Outlook’s square corners) | In diagnostics |
valid: true |
ElementsError
A template with errors makes render() reject, and outline() throw, with an ElementsError. Its diagnostics hold everything found, and its message lists the errors, one per line:
[Elements] The template has 1 error:
error style/unknown at root.children[0].style.marginBottom: "marginBottom" isn't a style a Text takes: use the parent's gap, or padding. …
formatDiagnostics(diagnostics) writes any list in that one-line form, for logs.
import { ElementsError, formatDiagnostics, render } from "@hyper-elements/react";
try {
const { html, diagnostics } = await render(<Receipt />);
if (diagnostics.length) console.warn(formatDiagnostics(diagnostics));
} catch (error) {
if (error instanceof ElementsError) console.error(formatDiagnostics(error.diagnostics));
throw error;
}
Codes you’ll meet
Errors: how the template is written
| Code | What triggers it |
|---|---|
jsx/markup |
An HTML element in a template (<b>, <br>, <div>), or around the root |
jsx/root |
Something beside the root, such as two roots |
jsx/async |
A component that’s async or suspends. Load its data first and pass it in as props |
jsx/error |
A component that threw inside a <Suspense> |
prop/unknown |
A prop the component doesn’t take, often a style written as a prop: <Email backgroundColor="#fff"> |
prop/required |
A required prop is missing: a Button’s href, an Image’s src |
prop/invalid |
A prop’s value isn’t one it takes, such as a javascript: link |
style/unknown |
A style the component doesn’t take: a margin, a root’s padding, an old name like align (the message names the new one) |
style/invalid |
A style’s value isn’t one it takes: justifyContent: "around", fontWeight: 650 |
mobile/not-allowed |
A phone style the component doesn’t take on phones |
mobile/not-supported |
Phone styles in a Document or a Canvas, which have no phone width |
hidden/not-supported |
hidden in a Document or a Canvas |
hover/not-allowed |
A hover style the component doesn’t take |
children/not-allowed |
A child the component can’t hold: a Heading in a Text, a root inside a root |
children/text-not-allowed |
Loose text where it can’t go, such as straight in a Row. Put it in a Text or Heading |
children/one-only |
A second PageHeader or PageFooter in a Document |
id/duplicate |
Two components with one id |
id/invalid |
An id that doesn’t start with a letter |
mobile/invalid, hover/invalid, hidden/invalid |
A mobile, hover or hidden value that isn’t one they take |
target/unsupported |
A component the output can’t draw: an Svg or a PageNumber in an email (Gmail and classic Outlook drop inline SVG) |
document/page-number |
A PageNumber in a Page or a Canvas, which have no pages |
email/layer |
A Layer email can’t draw: in an email it’s an Image as wide as the Layer with one child over it |
custom/render |
A custom component’s render throws, or returns something other than one component |
custom/depth |
Custom components expanding into each other too deep, as when one contains itself |
custom/duplicate |
Two different component() definitions with one type |
Warnings and notes: what an output can’t show
| Code | Severity | What triggers it |
|---|---|---|
email/unsupported |
warning | A style or prop email can’t show. The message says what it shows instead: justifyContent: "space-between" starts children at the left |
email/outlook-degraded |
info | Shown as asked, except in classic Outlook: square corners, normal letter spacing |
email/degraded |
info | Not quite as asked anywhere the email is read, such as web fonts in Gmail |
email/image-width |
warning | An Image with no width: Outlook shows it at its file’s size. Give src the file’s width and height |
email/width |
warning | A child of a Row takes its own size and its size isn’t known, so Outlook can’t lay the Row out |
email/font-fallback |
warning | A font stack ends on a web font, so a client that can’t load it picks its own |
email/video-thumbnail |
warning | A Vimeo Video with no thumbnail, which can’t be found without the network |
email/size |
warning | The HTML is large enough for Gmail to clip it (render() only) |
email/style-size |
warning | The stylesheet is more than Gmail reads (render() only) |
web/unsupported |
warning | The same, for a page |
layout/shrunk |
warning | A Row’s children’s widths add up to more than it has. They’re narrowed to fit, as a browser does |
layout/overflow |
warning | Content wider than the space it has: a Row’s children at their minimum widths, a box wider than its parent, a Table’s columns wider than the Table |
style/ignored |
warning | A style that does nothing where it is, such as flex on a child that isn’t in a Row |
email/layer-phone |
info | On phones, an email keeps a Layer’s height and cuts its image at the sides |
hidden/everywhere |
warning | Hidden on phones and on desktop, so it never shows |
id/dropped |
info | An id inside a custom component used without one of its own is left out |
mobile/not-shown |
warning | A custom component’s mobile style that nothing it renders has a phone version of |
jsx/compat |
warning | A compat prop on a root, which is left out |
document/overflow |
warning | A page’s content looks taller than the page (an estimate) |
canvas/overflow |
warning | A Canvas’s content looks taller than the Canvas (an estimate) |
renderPdf() and renderImage() add codes of their own (pdf/…, image/…). See PDF and Image.
Where support data comes from
Each style and prop of each component declares how each output shows it: full, outlook (as asked, but classic Outlook shows something else), partial, or none (with what it shows instead). One pass reads those declarations for the styles and props you set, and turns each into a diagnostic. A test keeps the declarations true: every style and prop, at every value, either changes the output or gives a diagnostic. Nothing is dropped silently.
Each declaration names its evidence: captures of real classic Outlook for Windows, caniemail.com’s tables, the writer’s own output checked in Chromium and against a model of Outlook’s layout, or plain CSS. Some are marked as not checked yet.
Use it in CI and with agents
validate() is synchronous, writes nothing, and returns what’s wrong rather than throwing it, so it fits in a unit test:
import { expect, test } from "vitest";
import { validate } from "@hyper-elements/react";
import { Welcome } from "./emails/welcome";
test("the welcome email is valid and has no warnings", () => {
const { valid, diagnostics } = validate(<Welcome name="Ada" />);
expect(valid).toBe(true);
expect(diagnostics.filter((d) => d.severity === "warning")).toEqual([]);
});
Match on code, which is stable for tools to match on, and show people the message.
For an agent, validate() is the feedback loop. Each message says what to use instead and where, so an agent that writes a template, validates it and reads the messages can fix its own mistakes: a margin becomes the parent’s gap, a style written as a prop goes into style. Pair it with outline() to check the layout as text, without reading HTML.
Tip: Point your agent at the package’s guide,
node_modules/@hyper-elements/react/GUIDE.md, before it writes a template. It lists every component on a line, and the mistakes that most often fail a first try.