Every word
In the same order, numbers, currency and percentages included. Nothing lost, nothing added.
Migrate
One command converts every template, keeps its props, loops and conditions, then renders the original and the migration side by side and proves the content survived. Nothing is written until you say so.
Before: React Email + Tailwind
import { Body, Button, Container, Head, Heading, Html, Preview, Tailwind, Text } from "@react-email/components";
export default function Welcome({ name = "Ada" }: { name?: string }) {
return (
<Html>
<Head />
<Preview>Welcome to Orbit</Preview>
<Tailwind>
<Body className="bg-gray-100 font-sans">
<Container className="mx-auto my-10 max-w-[520px] rounded-lg bg-white p-8">
<Heading className="m-0 text-2xl font-bold text-gray-900">Welcome, {name}</Heading>
<Text className="text-base text-gray-600">Your workspace is ready.</Text>
<Button href="https://example.com/start" className="rounded-md bg-indigo-600 px-5 py-3 text-white">
Open your workspace
</Button>
</Container>
</Body>
</Tailwind>
</Html>
);
}
Welcome.PreviewProps = { name: "Ada" };After: Hyper Elements, written by the migration
// Migrated from React Email by @hyper-elements/migrate (codemod mode).
import {
Button,
Column,
Email,
Heading,
Section,
Text,
} from "@hyper-elements/react";
export default function Welcome({ name = "Ada" }: { name?: string }) {
return (
<Email
width={520}
previewText="Welcome to Orbit"
style={{
backgroundColor: "#f3f4f6",
color: "#000000",
fontFamily:
"ui-sans-serif,system-ui,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji'",
}}
>
<Section style={{ padding: [40, 0, 40, 0] }}>
<Column
style={{ padding: "32px 32px 32px 32px", backgroundColor: "#ffffff", borderRadius: 8 }}
>
<Heading
style={{
padding: "0px 0px 0px 0px",
fontSize: 24,
fontWeight: 700,
color: "#101828",
lineHeight: 1.3333333333333333,
}}
>
Welcome, {name}
</Heading>
<Text
style={{
padding: "16px 0px 16px 0px",
fontSize: 16,
color: "#4a5565",
lineHeight: 1.5,
}}
>
Your workspace is ready.
</Text>
<Column style={{ padding: "0px 0px 0px 0px", alignItems: "start" }}>
<Button
href={{ href: "https://example.com/start", target: "_blank" }}
style={{
backgroundColor: "#4f39f6",
color: "#ffffff",
fontSize: 16,
lineHeight: "120%",
padding: "12px 20px 12px 20px",
borderRadius: 6,
}}
>
Open your workspace
</Button>
</Column>
</Column>
</Section>
</Email>
);
}
Welcome.PreviewProps = { name: "Ada" };Real output: Tailwind classes became typed styles, margins became padding, the container became the email's width. ✓ 100% editable
Checked, not hoped
After converting, the tool renders both versions and compares what a reader gets. Content that differs fails the run with exit code 2. How things look is listed for you to review.
In the same order, numbers, currency and percentages included. Nothing lost, nothing added.
Each href, each image and its alt text, on both sides.
Again at 375px, and at each wider width where a rule like Tailwind's md: applies.
Where either side has a prefers-color-scheme rule: a logo or text swapped for dark mode.
Rendered with your PreviewProps, then with each boolean prop flipped, so conditional content is checked too.
Each word's size, weight, color and place on the page. Differences are listed for you, not hidden.
106 of 106
real React Email templates convert with no content lost. 96 are fully editable in the visual editor, and 98.4% of their content is on average.
git-safe
--write refuses files git can't restore unless you pass --allow-dirty, so every migration can be undone. --strict fails on any difference at all.