Skip to content

Previewing React Email templates locally, and what the preview cannot tell you

React Email's preview server renders your templates with realistic props and hot reload. Here is how to set it up, what to check, and the four failure modes only a real client will show you.

Resend4 min readships at docs/solutions/resend/previewing-templates-locally.md

Tags: resend · react-email · preview · templates · testing · dev

Email is the last place in a modern stack where you cannot see what you built until someone receives it. React Email fixes most of that: templates are components, they render in a browser, and they hot reload.

Most of it. There is a specific list of things the preview cannot show you, and knowing where the line is saves you from shipping a message that looked perfect locally and arrives broken in Outlook.

Running the preview

bun run email:dev

That runs email dev --dir src/lib/email/templates, the CLI that ships with the react-email package. Point it at the directory where the templates live rather than moving them to the tool's default location: templates belong next to the code that sends them, not in a top-level folder that looks like content.

It opens a browser with every template in the directory listed down the side. It watches the files, so editing a component re-renders immediately.

Each template needs a default export to appear. A named export renders nothing and the tool does not explain why, which costs people twenty minutes the first time.

PreviewProps: realistic data, no credentials

MagicLinkEmail.PreviewProps = {
  url: "https://example.com/auth/verify?token=preview-token-not-a-real-credential",
  expiresInMinutes: 15,
  requestedFrom: "Chrome on macOS, Lisbon",
} satisfies MagicLinkEmailProps;

satisfies is doing real work: change a prop type and the preview data fails to typecheck instead of silently rendering undefined.

Two rules for the data itself. Make it realistic: a name of "Test" and a total of $1.00 hide the layout bugs that a 40-character company name and $1,234.56 expose. And never put a real credential in there; preview props sit in your repository forever.

What to check in the preview

  • The plain-text tab. Read it as if it were all you had. If the message makes no sense without the button, the copy is wrong, and a text-only client is exactly what some recipients use.
  • The <Preview> line. It is the grey text next to the subject in an inbox. Without one, the client shows the first words of the body, which is usually "View this email in your browser" or an alt attribute.
  • Long values. A 60-character name, a nine-line item list, a URL with no spaces. Overflow is the most common template bug and the easiest to find.
  • Empty and singular states. One line item, zero line items, no optional prop. Optional props are optional at runtime whether or not you rendered them in the preview.
  • The rendered HTML source. If you see a <div> doing layout, Outlook will disagree with you. React Email's components emit tables for a reason.

Rendering to HTML in a test

For a snapshot test or a script:

// `render` comes from react-email, which re-exports @react-email/render. Resend
// renders the `react` field with that same package on the server.
import { render } from "react-email";
import WelcomeEmail from "@/lib/email/templates/welcome";

const html = await render(WelcomeEmail({ name: "Sam", ctaUrl: "https://example.com/app" }));
const text = await render(WelcomeEmail({ name: "Sam", ctaUrl: "https://example.com/app" }), {
  plainText: true,
});

Worth asserting in a unit test: that the CTA URL appears in both outputs, that no undefined string leaked in, and (for anything security-relevant) that a token appears exactly once, in the href.

What the preview cannot tell you

1. How Outlook renders it. The Windows Outlook clients use the Word rendering engine, which ignores flexbox, grid, most positioning and many CSS properties. Your browser preview will not warn you. Staying inside React Email's components (imported from react-email) keeps you on the safe path, and adding raw markup is where people leave it.

2. Whether it lands in the inbox. Placement depends on authentication, domain reputation, content signals and the recipient's own filters. None of that exists locally. bun run email:send-test you@example.com is how you find out.

3. Whether Gmail clips it. Gmail truncates around 102KB of HTML and hides the rest behind "View entire message". Inline data URIs eat that budget fast. Check the rendered size, not the source size.

4. How it looks in dark mode. Some clients invert colours, some respect prefers-color-scheme, some do neither. The templates here are monochrome and inherit the client's own palette specifically so this cannot go wrong: the moment you hardcode a colour, you own that problem in every client.

The loop that works

  1. Build in the preview until the layout and copy are right.
  2. bun run email:send-test you@example.com.
  3. Open it on a phone and on a desktop client. Check the subject is not cut off at 40 characters, that the links survived the client rewriting them, and that it landed in the inbox rather than Promotions.
  4. If you support enterprise customers, get one real Outlook screenshot before you ship. Once, per template. It is the only way to know.

Steps 1 and 2 are fast enough to do on every change. Steps 3 and 4 are worth it per template, not per edit.