v0.3Open source, MIT licensed

The content engine
for agents.

Your agent writes React. Hyper Elements turns it into emails, pages, PDFs and images, checks it in every client, and tells the agent how to fix what's wrong.

Explore components →
render.tsx
const out = await render(
  <Email>
    <TarnShipped />
  </Email>
);
// → tarn-shipped.html + .txt
TTarnYour Tarn order has shipped9:41 AM
●casaalbero.example
PDFpell-ashby-invoice.pdfDownload ↓
PNGgridline-workshop-banner.png1200 × 627
Gridline workshop banner: A LinkedIn event banner, 1200 by 627: a title, the details in pills, the hosts, and a grid of shapes drawn in Svg.
Agent benchmark2× fasterthan HTML or React EmailTokens written2.4× fewerthe engine writes the tables
RendersOne templateEmail, web page, PDF or image
Built forAny agentClaude Code, Codex, Cursor, Copilot

The agent loop

Create, check, validate, fix. Around again until the content is right.

  1. 01

    Create

    Your agent writes JSX

    “Write a launch email for Orbit 2.0”
    <Email style={{
      backgroundColor: "#f1f2f6",
      padding: 24
    }}>
      <Heading>Orbit 2.0…
      <Button href="…">Try it
    </Email>
  2. 02

    Check

    See it in every client

    The email in Classic Outlook
    ⚠Classic Outlook
    The email in Gmail
    Gmail
    The email in Apple Mail
    Apple Mail
    The email in Apple Mail dark
    ⚠Apple Mail dark

    ⚠ Outlook drops the button's padding

  3. 03

    Validate

    Errors, lint and spam score

    validate()1 error

    style/unknown root.style.padding

    "padding" isn't a style an Email takes: a root has no padding; put it on a Column (a card) or a Section (a band) inside it.

    Lint3 warnings

    a11y/link-text "Try it" alone

    best-practice/preview-text

    deliverability/unsubscribe

    Spam score3.4 / 10

    +1.6 no unsubscribe link

    +0.9 vague call to action

  4. 04

    Fix

    Each finding says how

    - <Email style={{padding: 24}}>
    + <Email previewText="…">
    +   <Section
    +     style={{padding: 24}}>
    -     <Button …>Try it
    +     <Button …>Try Orbit 2.0
    +     <Link href={unsubscribe}>
    +   </Section>
    ✓ valid✓ lint clean✓ spam 0.6

↺ Around again until every check passes and every client looks right

Build with any AI

Point your agent at the guide, then ask for what you want.

1Give it the guide

CLAUDE.md
@node_modules/@hyper-elements/react/GUIDE.md

The guide ships inside the package, written from the components' own definitions, so it matches the version you installed.

2Ask for what you want

Using Hyper Elements, write a launch email for our new feature in emails/launch.tsx. Run validate() and fix every finding, then show me outline().

How agents use Hyper Elements →

How it works

A short template and its real HTML. Resize it: rows stack and phone styles apply.

emails/plan-picker.tsx
import { Button, Column, Email, Heading, Text, Row, Section } from "@hyper-elements/react";
import type { ComponentProps } from "react";
import type { ButtonProps, ColumnProps } from "@hyper-elements/react";

const plans = [["Starter", "$0"], ["Pro", "$24"], ["Team", "$79"]];

export default function PlanPicker() {
  return (
    <Email style={{ backgroundColor: "#f1f2f6" }}>
      <Section style={{ padding: 20 }}>
        <Column style={card}>
          <Heading style={{ fontSize: 30 }}
            mobile={{ fontSize: 22 }}>
            Pick your Orbit plan
          </Heading>
          <Row style={{ gap: 12 }}>
            {plans.map(([name, price]) => (
              <Column key={name} style={plan}>
                <Text style={label}>{name}</Text>
                <Text style={amount}>{price}</Text>
              </Column>
            ))}
          </Row>
          <Button href="https://orbit.dev/plans" style={cta}
            hover={{ backgroundColor: "#4c3df0" }}>
            Choose a plan
          </Button>
        </Column>
      </Section>
    </Email>
  );
}

const card = { backgroundColor: "#fff", borderRadius: 12,
  padding: 28, gap: 18 };
const plan: ColumnProps["style"] = { backgroundColor: "#f4f3ff", borderRadius: 10,
  padding: 16 };
const label: ComponentProps<typeof Text>["style"] = { fontWeight: 700, color: "#6d5efc" };
const amount: ComponentProps<typeof Text>["style"] = { fontSize: 26, fontWeight: 700 };
const cta: ButtonProps["style"] = { backgroundColor: "#6d5efc", borderRadius: 8,
  padding: [12, 22] };
680px
Row 3 side by sideHeading 30pxDrag the edge, or pick a width

Features

Type safety

<Button style={{ font }}>
  • fontSizelength
  • fontWeight100 to 900
  • fontFamilyfont stack

Personalization

Hi <MergeTag name="first_name" />
  →  in the HTML and the text:
Hi {{first_name}}

Your provider's syntax with
the mergeTag option.

Plain text

[Orbit]
INTRODUCING ORBIT 2.0
SHIP EVERY SURFACE FROM ONE CODEBASE.
…

Client compatibility

info email/outlook-degraded
borderRadius on this Column in classic Outlook: square corners

Deterministic output

render(<Launch />)
  ===
render(<Launch />)  ✓

No clock, no randomness.
Safe to cache and prerender.

Security

<Text>Hi {name}</Text>
  → Hi &lt;img onerror=…&gt;

<Button href="javascript:…">
error prop/invalid
isn't a link Elements will write

Every value escaped. No eval: runs under a strict CSP.

Custom components

const ProductCard = component({
  type: "acme.ProductCard",
  props: { name: prop.text({ required: true }),
           photo: prop.image({ required: true }) },
  render: ({ name, photo }) => …,
});
<ProductCard name="Linen shirt" />

error prop/required
A acme.ProductCard needs "photo"

Named, typed blocks your team and your agents reuse, checked like the built-ins.

Get started