Skip to main content

Our email CSS engine is open source. Give it a star on GitHub 

Emailens
Blog

jsx-email & React Email Bugs: withMargin, renderWhitespace, pxToPt

Most React Email and jsx-email bugs come from three gaps between JSX and the inbox: margins (including withMargin) that Outlook Classic ignores, spaces renderWhitespace loses between inline elements, and pxToPt values that round badly.

The checker needs a computer screen.

A free account opens all 21 clients and keeps your checks for 7 days. Create a free account →

Philippe Kam, founder of Emailens · Published 26 Sep 2026 · Updated 28 Oct 2026

@emailens/engine 0.18.22 · data last changed 10 Nov 2026

Part of the HTML email guide · all guides

If you searched for withMargin, withSpace, renderWhitespace or pxToPt with @jsx-email or React Email, the bug behind it is almost always one of three: spacing that Outlook Classic ignores, spaces that disappear between inline elements, or a px-to-pt value that rounds badly. All three look correct in the preview server and break only in a real inbox. This page gives the cause and the patch for each, plus MSO conditional comments in JSX and a way to check the render() output. For setting up a project, see the React Email guide.

withMargin and CSS margin: why Outlook ignores the spacing

Both @react-email/components and @jsx-email let you apply spacing via the style prop, including CSS margin, and spacing helpers such as withMargin end up as the same inline margin in the HTML. This works in Apple Mail, Gmail web, and iOS Mail, all clients that render block-level CSS margin. But Classic Outlook on Windows (2016, 2019, 2021) uses the Microsoft Word rendering engine, which ignores CSS margin on most elements.

The bug is subtle because the React Email preview server uses a standard browser renderer, so the layout looks right in development, then loses its spacing when the email is opened in Outlook.

section-spacing.tsx (broken)
10 lines
// ❌ Broken: CSS margin via style prop is ignored by Outlook Classic
import { Section, Row, Column, Text } from "@react-email/components";

// Both @react-email and @jsx-email let you pass CSS margin via style prop
<Section style={{ margin: "24px 0" }}>
  <Text>Hero content</Text>
</Section>

// Renders: <table style="margin: 24px 0">, Outlook Word engine ignores it
// Result: zero vertical gap above and below the section in Classic Outlook
section-spacing.tsx (fixed)
18 lines
// ✅ Fixed: Replace margin with padding on the wrapping Column
import { Section, Row, Column } from "@react-email/components";

// Use explicit padding on the structural table cell instead
<Section>
  <Row>
    <Column style={{ padding: "24px 0" }}>
      <Text>Hero content</Text>
    </Column>
  </Row>
</Section>

// For pure vertical spacers, use a dedicated spacer row:
<Row>
  <Column style={{ height: "24px", lineHeight: "24px", fontSize: "0" }}>
    &nbsp;
  </Column>
</Row>
Four ways Outlook Classic mishandles margin
  • auto is unsupported: margin: 0 auto fails to center elements in Classic Outlook. Containers require ghost tables with explicit width constraints.
  • Background bleed: Classic Outlook includes background-color inside the margin box, causing colored card backgrounds to spill into the margin spacing.
  • Negative values are ignored: Negative margin pulls do not work in Outlook on Windows or mobile.
  • Inline elements unsupported: Margin on <span> and <body> is discarded.

Button padding exception: React Email's <Button> primitive avoids margin entirely, using mso-font-width: -100% and a hair space (&#8202;) to achieve reliable horizontal padding in Outlook without VML.

For centering and multi-column layouts in Outlook, see Outlook ghost tables.

renderWhitespace: words run together between inline elements

The symptom: words run together without spaces, “Read ourdocumentationfor more details.”, in the rendered HTML, even though the JSX source looks correct with text separated across multiple lines.

The cause is how JSX handles whitespace. During compilation, adjacent text nodes and inline elements separated by newlines have their whitespace collapsed. A browser app behaves the same way, but in email, where the render() function serializes React output directly to an HTML string, the missing space ends up in every inbox.

text-with-link.tsx (broken)
9 lines
// ❌ Broken: whitespace between inline elements collapses
import { Text, Link } from "@react-email/components";

<Text>
  Read our
  <Link href="/docs">documentation</Link>
  for more details.
</Text>
// Renders as: "Read ourdocumentationfor more details.", no spaces
text-with-link.tsx (fixed)
16 lines
// ✅ Fixed: insert explicit {' '} string literals between inline nodes
import { Text, Link } from "@react-email/components";

<Text>
  Read our{' '}
  <Link href="/docs">documentation</Link>
  {' '}for more details.
</Text>
// Renders as: "Read our documentation for more details." ✓

// The same rule applies to <span> inside <Text>:
<Text>
  Order{' '}
  <span style={{ fontWeight: 700 }}>#12345</span>
  {' '}has shipped.
</Text>

The {' '} pattern is the standard React solution and is email-safe. It renders as a literal space character in the HTML output and survives email client sanitizers, including Gmail's <style> stripping, since it is a text node, not a CSS rule. See also what Gmail's sanitizer actually strips.

pxToPt: rounding px to pt for Outlook font sizes

Some components in both @react-email/components and @jsx-email internally convert pixel values to point units for compatibility with Outlook's MSO VML attribute system. The conversion factor is 1px = 0.75pt (or equivalently, 1pt = 1.333px).

The bug: when this conversion produces a fractional result (e.g., 15px → 11.25pt), the MSO rendering engine rounds in an implementation-defined manner, sometimes up, sometimes down, and occasionally it refuses to apply the value at all, falling back to a default like 12pt Times New Roman. This is the cause of many “Outlook uses the wrong font size” reports.

px-to-pt.tsx
20 lines
// ❌ Unsafe: fractional pt values crash MSO rendering
const fontSizePt = 15 / 1.333; // → 11.25pt, MSO rounds unpredictably

// ✅ Safe: always Math.round() before inserting into MSO attributes
const pxToPt = (px: number) => Math.round(px / 1.333);

// Usage in a component with MSO conditional fallback:
const fontSize = 15; // design intent in px

<Text
  style={{
    fontSize: `${fontSize}px`,
    // mso-font-size is picked up only by Classic Outlook (Word engine)
    // @ts-expect-error, MSO vendor attribute not in React types
    "mso-font-size": `${pxToPt(fontSize)}pt`,
    lineHeight: "24px",
  }}
>
  Safe cross-client text
</Text>

The mso-font-size vendor attribute is not part of the TypeScript React types, so you will need a @ts-expect-error suppression comment or a custom type augmentation. This is expected: the attribute is Outlook-proprietary and should only be used inside MSO conditional fallbacks in production templates.

Tailwind v4 classes missing in Gmail

If your styles or breakpoints stopped working in Gmail after upgrading to Tailwind CSS v4, the cause is not React Email or jsx-email itself: Tailwind v4 writes media queries in the range syntax and nests them inside selectors, and Gmail reads neither. The fix, which clients are affected and what downleveled CSS looks like are in why Tailwind v4 emails break in Gmail.

MSO conditional comments in JSX: dangerouslySetInnerHTML and ghost tables

Classic Outlook on Windows relies on MSO conditional comments to trigger VML and ghost table structures. The challenge in React Email is that JSX comments ({/* ... */}) are stripped by the React compiler and never appear in the serialized HTML string. And because JSX requires every tag to be closed, you cannot place an unclosed opening <tr><td> tag in a component without a syntax error.

There are two working patterns:

  • Component-level dangerouslySetInnerHTML: The pattern the Emailens Engine fix database uses for Outlook max-width ghost tables, VML rounded buttons, and VML backgrounds. Because the markup is passed as a string literal, React outputs raw, unescaped HTML comments without a build step.
  • Post-processing the render output: If you prefer declarative JSX trees without inline HTML strings, render custom placeholder tags (e.g. <mso-ghost-table>) and run a string replacement on the output of render() before sending.
mso-conditionals.tsx
55 lines
// ✅ Pattern 1: Component-Level MSO Ghost Table via dangerouslySetInnerHTML
// (This is the official pattern implemented in the Emailens Engine fix database)
// In JSX, every tag must be strictly closed, you cannot place an unclosed <tr> in JSX.
// dangerouslySetInnerHTML outputs raw HTML strings containing unescaped MSO conditionals:

export function OutlookGhostContainer({
  width = 600,
  childrenHtml,
}: {
  width?: number;
  childrenHtml: string;
}) {
  return (
    <div
      dangerouslySetInnerHTML={{
        __html: `<!--[if mso]>
<table role="presentation" width="${width}" align="center" cellpadding="0" cellspacing="0" border="0"><tr><td>
<![endif]-->
<div style="max-width:${width}px;margin:0 auto;">
  ${childrenHtml}
</div>
<!--[if mso]>
</td></tr></table>
<![endif]-->`,
      }}
    />
  );
}

// Pattern 2: Outlook VML Button with border-radius fallback
export function VmlButton({ href, label }: { href: string; label: string }) {
  return (
    <div
      dangerouslySetInnerHTML={{
        __html: `
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
  href="${href}"
  style="height:44px;v-text-anchor:middle;width:200px;"
  arcsize="14%" strokecolor="#6d28d9" fillcolor="#6d28d9">
  <w:anchorlock/>
  <center style="color:#ffffff;font-family:Arial,sans-serif;font-size:14px;font-weight:bold;">${label}</center>
</v:roundrect>
<![endif]-->
<!--[if !mso]><! -->
<a href="${href}"
  style="background-color:#6d28d9;color:#ffffff;padding:12px 32px;border-radius:6px;text-decoration:none;display:inline-block;font-weight:bold;">
  ${label}
</a>
<!-- <![endif]-->
`,
      }}
    />
  );
}

Whichever pattern you choose, [if mso] and [endif] must appear inside real HTML comment markers (<!-- -->) in the final sent markup. For the MSO syntax itself, see MSO conditional comments for Outlook.

Check the render() output before you send

The React Email preview server renders components inside a browser iframe with full CSS support, which creates false confidence. The production pipeline calls render() on the server and sends the raw HTML string. Bugs that only appear in Outlook, Gmail Android, or Samsung Email are usually invisible in the preview server.

The fastest check is a local script that calls render() directly, writes the output to an HTML file, and opens it in a browser. This catches most JSX whitespace, conditional comment, and serialization issues before you reach an email client. Note: renderAsync() was the v2 API and is deprecated in React Email v3+; use render() from @react-email/render.

debug-render.ts
18 lines
// Debugging server render output locally
// Note: renderAsync() was deprecated in React Email v3. Use render() instead.
import { render } from "@react-email/render";
import { MyEmail } from "./emails/my-email";

async function debugRender() {
  const html = await render(<MyEmail />, {
    pretty: true,       // readable indented output
    plainText: false,
  });

  // Write to a temp .html file and open in browser for quick visual check
  const fs = await import("fs/promises");
  await fs.writeFile("debug-output.html", html, "utf-8");
  console.log("Rendered", html.length, "bytes");
}

debugRender().catch(console.error);

For issues that only appear inside a real inbox (Outlook margin gaps, Gmail class stripping, Apple Mail dark mode), the next step is a cross-client render test. Emailens previews your HTML live in 21 email clients, with no account and no seed list. The full testing workflow is in how to test HTML emails.

React Email vs jsx-email: the same bugs in both

@jsx-email is a community fork of @react-email that was created partly in response to slow issue triage in the original repo. It ships several bug-fixed component variants (Text, Button, Section) and focuses on faster releases. Both libraries expose the same styling API via the style prop and Tailwind, so the margin and Outlook problems above apply to both.

Issue@react-email@jsx-email
Whitespace collapse between inline elementsPossible, use {' '}Possible, same JSX rules
CSS margin ignored by Classic OutlookAffected, use padding on ColumnAffected, use padding on Column
px-to-pt conversion bugsPossible in some componentsPossible, Math.round required
Tailwind v4 breakpoints dropped by GmailAffected, downlevel the CSSAffected, downlevel the CSS
MSO conditional comment escapingdangerouslySetInnerHTML or post-processdangerouslySetInnerHTML or post-process

Both libraries serialize JSX to HTML strings, then send that HTML into email clients that were never designed for component-based rendering. The bugs on this page are email-environment bugs, not library bugs: they would exist in any JSX-to-HTML pipeline.

Questions

Why do margins (and withMargin) on React Email and @jsx-email components break in Outlook?

According to the Emailens Engine compatibility matrix, Outlook on Windows (Word engine) evaluates CSS margin with several critical caveats: 'auto' is not supported (causing 'margin: 0 auto' on Container to fail centering), negative margins are discarded, margin is unsupported on <span> and <body>, and background-color bleeds into the margin space instead of stopping at the padding edge. The robust fix is to use padding on <Column> or <td> elements, or use dedicated table spacer rows. For interactive CTAs, React Email's <Button> handles Outlook padding automatically using the mso-font-width and hair-space (&#8202;) technique.

What causes renderWhitespace errors in React Email and jsx-email, and how do I fix them?

renderWhitespace is triggered when JSX expressions between inline elements (e.g., <Text> with nested <Link> spans) collapse adjacent text nodes in a way that removes the expected space character. This happens because React's JSX compiler trims whitespace between sibling elements during transpilation. The fix is to insert an explicit {' '} expression, a JavaScript string literal containing a single space, between inline elements where whitespace must be preserved.

How do I write a safe pxToPt conversion in React Email for Outlook?

Some React Email components apply pt units for font sizes to satisfy Outlook's MSO VML renderer. The standard conversion is 1pt = 1.333px (or equivalently 1px ≈ 0.75pt). However, fractional pt values in MSO attributes are rounded unpredictably by the Word engine. Always round converted values to the nearest integer and use px for all web-facing clients via inline CSS, reserving pt only for mso-font-size attributes in MSO conditional comments.

Your next question

Also useful: HTML email checker · Test React Email templates · display:flex in Outlook Classic: the fix

Paste the email that broke.

The checker needs a computer screen.

Get new email CSS fixes by email, or follow by RSS.