npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-email-mso

v0.5.2

Published

MSO conditional comments for react-email. Outlook-safe rendering with zero dependencies.

Readme

react-email-mso

MSO conditional comments for react-email. Outlook-safe rendering with zero dependencies.

React can't output HTML comments, which makes <!--[if mso]> impossible. This package solves it with custom HTML elements that pass through React's renderer untouched, then a simple string post-processor converts them to real MSO conditional comments.

~20 lines of code. Zero dependencies. Works with react-email, jsx-email, or plain react-dom/server.

Install

npm install react-email-mso
# or
bun add react-email-mso

Requires React 19+.

Quick Start

import { render } from '@react-email/render';
import { Outlook, processConditionals } from 'react-email-mso';

const Email = () => (
  <Html>
    <Body>
      <Outlook fallback={<table><tbody><tr><td width="600">Ghost table for Outlook</td></tr></tbody></table>}>
        <div style={{ maxWidth: 600 }}>Modern layout</div>
      </Outlook>
    </Body>
  </Html>
);

const html = processConditionals(await render(<Email />));

Output:

<!--[if mso]>
  <table><tbody><tr><td width="600">Ghost table for Outlook</td></tr></tbody></table>
<![endif]-->
<!--[if !mso]><!-->
  <div style="max-width:600px">Modern layout</div>
<!--<![endif]-->

API

<Outlook>

One component, two modes.

Paired mode (most common)

Use fallback to provide Outlook-specific markup alongside your modern default. Children are the default (modern clients), fallback is what Outlook gets.

<Outlook fallback={<table><tbody><tr><td>Outlook gets this</td></tr></tbody></table>}>
  <div>Modern clients see this</div>
</Outlook>

Standalone mode

Render content for only Outlook, or only non-Outlook clients.

<Outlook>
  <table><tbody><tr><td>Only Outlook sees this</td></tr></tbody></table>
</Outlook>

<Outlook not>
  <div>Everything except Outlook sees this</div>
</Outlook>

The not prop uses the downlevel-revealed comment pattern, which is required for non-MSO clients to see the content. Do not use expr="!mso" for this — it produces a normal conditional comment that is invisible to all clients.

Version targeting

Target specific Outlook versions with expr.

<Outlook expr="gte mso 9">
  <style>{'body { font-family: Calibri; }'}</style>
</Outlook>
{/* Output: <!--[if gte mso 9]><style>...</style><![endif]--> */}

Works with fallback too:

<Outlook expr="gte mso 9" fallback={<table><tbody><tr><td>Outlook 9+ gets this</td></tr></tbody></table>}>
  <div>Everyone else sees this</div>
</Outlook>

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | children | ReactNode | required | Default content (modern clients in paired mode, Outlook content in standalone mode) | | not | boolean | false | Use downlevel-revealed pattern — content visible to non-Outlook clients | | expr | string | 'mso' | Conditional expression (e.g. "gte mso 9") | | fallback | ReactNode | — | Outlook-specific content (enables paired mode when provided) |

Outlook Version Numbers

| Outlook Version | MSO Number | |-----------------|------------| | Outlook 2000 | 9 | | Outlook 2002/XP | 10 | | Outlook 2003 | 11 | | Outlook 2007 | 12 | | Outlook 2010 | 14 | | Outlook 2013 | 15 | | Outlook 2016 / 2019 / 365 | 16 |

Operators: gt, lt, gte, lte, !

processConditionals(html: string): string

Converts custom elements to MSO conditional comments. Call this on the HTML string returned by render().

import { render } from '@react-email/render';
import { processConditionals } from 'react-email-mso';

const html = processConditionals(await render(<MyEmail />));

Important: If using render({ pretty: true }), the pretty-printing runs inside render() before processConditionals sees the output. This is the correct order — Prettier handles custom elements fine but would break MSO conditional comment syntax.

Blocks

Higher-level components that use <Outlook> internally to automatically render the right markup for each client.

<BulletproofButton>

A button that renders VML v:roundrect for Outlook and a styled <a> tag for modern clients.

import { BulletproofButton, processConditionals } from 'react-email-mso';

const Email = () => (
  <BulletproofButton
    href="https://example.com"
    color="#EB7035"
    textColor="#ffffff"
    width={200}
    height={44}
    borderRadius={4}
    fontSize={16}
    fontFamily="Helvetica, Arial, sans-serif"
  >
    Get Started
  </BulletproofButton>
);

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | href | string | required | Button link URL | | children | ReactNode | required | Button text (strings or JSX) | | color | string | '#007bff' | Background color | | textColor | string | '#ffffff' | Text color | | width | number | 200 | Width in pixels | | height | number | 40 | Height in pixels | | borderRadius | number | 4 | Border radius in pixels (converted to VML arcsize) | | fontFamily | string | 'sans-serif' | Font family | | fontSize | number | 16 | Font size in pixels |

What it renders

Outlook (inside <!--[if mso]>):

<v:roundrect href="https://example.com"
  style="height:44px;v-text-anchor:middle;width:200px;"
  arcsize="18%" stroke="f" fillcolor="#EB7035">
  <w:anchorlock/>
  <center style="color:#ffffff;font-family:Helvetica, Arial, sans-serif;font-size:16px;">
    Get Started
  </center>
</v:roundrect>

Modern clients (inside <!--[if !mso]><!-->):

<a href="https://example.com"
  style="background-color:#EB7035;border-radius:4px;color:#ffffff;
         display:inline-block;font-family:Helvetica, Arial, sans-serif;
         font-size:16px;font-weight:bold;line-height:44px;
         text-align:center;text-decoration:none;width:200px">
  Get Started
</a>

<Columns> / <Column>

Responsive multi-column layout. Outlook gets a fixed-width ghost table; modern clients get inline-block divs that stack on mobile.

import { Columns, Column, processConditionals } from 'react-email-mso';

const Email = () => (
  <Columns gap={20}>
    <Column width={280}>
      <img src="product1.jpg" width={280} />
      <p>Wireless Headphones — $199</p>
    </Column>
    <Column width={280}>
      <img src="product2.jpg" width={280} />
      <p>Smart Watch — $299</p>
    </Column>
  </Columns>
);

Props

Columns:

| Prop | Type | Default | Description | |------|------|---------|-------------| | children | Column[] | required | Column elements | | gap | number | 0 | Gap between columns in pixels |

Column:

| Prop | Type | Default | Description | |------|------|---------|-------------| | width | number | required | Column width in pixels | | children | ReactNode | required | Column content |

How It Works

React can't output HTML comments. Every attempt — string interpolation, dangerouslySetInnerHTML — gets escaped or stripped.

The insight: Custom HTML elements (names with a hyphen) pass through every React renderer untouched. React treats them as web components and renders them literally.

This package uses two custom elements as markers:

| Element | Becomes | |---------|---------| | <mso-expr data-expr="X"> | <!--[if X]>...<![endif]--> | | <mso-else-expr data-expr="X"> | <!--[if X]><!-->...<!--<![endif]--> |

<Outlook> emits these custom elements based on its props. Then processConditionals() does a single-pass regex replacement on the final HTML — no AST parsing, no rehype pipeline, no custom renderer.

Constraints

  • React 19+ required. React 18's SSR silently drops children of custom elements (facebook/react#27403). React 19 fixed this. react-email v2 already uses React 19 streaming renderers.
  • Don't nest <Outlook> inside <Outlook>. MSO conditional comments can't be nested — Outlook's parser will misinterpret it. Use a single <Outlook> wrapper per block.
  • VML requires XML namespace declarations on your <html> tag for Outlook to render VML elements:
<html xmlns:v="urn:schemas-microsoft-com:vml"
      xmlns:o="urn:schemas-microsoft-com:office:office">

License

MIT